diff -pruN 3.24.0-1/.gitlab/issue_templates/Default.md 3.26.0-1/.gitlab/issue_templates/Default.md
--- 3.24.0-1/.gitlab/issue_templates/Default.md	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/.gitlab/issue_templates/Default.md	2025-08-12 11:26:23.000000000 +0000
@@ -1,6 +1,6 @@
 # Checklist
 
-- [ ] The issue remains in the [development version of ASE](https://wiki.fysik.dtu.dk/ase/install.html#installation-from-source).
+- [ ] The issue remains in the [development version of ASE](https://ase-lib.org/install.html#installation-from-source).
 - [ ] An minimal example is provided to reproduce the issue.
 
 # Summary
diff -pruN 3.24.0-1/.gitlab/merge_request_templates/Default.md 3.26.0-1/.gitlab/merge_request_templates/Default.md
--- 3.24.0-1/.gitlab/merge_request_templates/Default.md	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/.gitlab/merge_request_templates/Default.md	2025-08-12 11:26:23.000000000 +0000
@@ -1,8 +1,9 @@
 Here: Your concise description of the content of this merge request.
 
-# Checklist
+# Checklist: `x` (done) or `~` (irrelevant)
 
-- [ ] I am familiar with [ASE's contribution guidelines](https://wiki.fysik.dtu.dk/ase/development/contribute.html).
-- [ ] [**Doc strings**](https://wiki.fysik.dtu.dk/ase/development/python_codingstandard.html#writing-documentation-in-the-code) in code changed in this MR are up to date.
-- [ ] [**Unit tests** have been added for new or changed code.](https://wiki.fysik.dtu.dk/ase/development/tests.html)
+- [ ] [I am familiar with **ASE's contribution guidelines.**](https://ase-lib.org/development/contribute.html)
+- [ ] [**Doc strings** in code changed in this MR are up to date.](https://ase-lib.org/development/python_codingstandard.html#writing-documentation-in-the-code)
+- [ ] [**Unit tests** have been added for new or changed code.](https://ase-lib.org/development/tests.html)
+- [ ] [**Changes** have been added in `changelog.d` using `scriv`.](https://ase-lib.org/development/writing_changelog.html)
 - [ ] [**Issue is resolved** via "closes #XXXX" if applicable.](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
diff -pruN 3.24.0-1/.gitlab-ci.yml 3.26.0-1/.gitlab-ci.yml
--- 3.24.0-1/.gitlab-ci.yml	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/.gitlab-ci.yml	2025-08-12 11:26:23.000000000 +0000
@@ -2,57 +2,55 @@
 
 variables:
   OMP_NUM_THREADS: "1"
+  PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
+  PYTEST_ADDOPTS: "--color=yes --durations=20 -r s"
 
 stages:
   - test
   - paperwork
   - deploy
 
-# Test a non-standard port (default is 3306) to prevent bugs like
-# https://gitlab.com/ase/ase/-/merge_requests/2789 from reoccuring
-# UPDATE: MYSQL_TCP_PORT does not work at all with gitlab-runner.
-# No matter what the variable is set to, it errors out.
-# Thus (temporarily?) disabled as of April 2023.
-.database-configuration:
-  variables:
-    POSTGRES_DB: testase
-    POSTGRES_USER: ase
-    POSTGRES_PASSWORD: "ase"
-    MYSQL_DATABASE: testase_mysql
-    MYSQL_ROOT_PASSWORD: ase
-    # MYSQL_TCP_PORT: 3306
-
-  services:
-    - postgres:latest
-    - mysql:latest
-    - mariadb:latest
+# This will ensure that only pipelines in master get to update the cache,
+# see https://docs.gitlab.com/ci/caching/
+# conditional-policy:
+#  rules:
+#    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+#      variables:
+#        POLICY: pull-push
+#    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
+#      variables:
+#        POLICY: pull
 
 # Check oldest supported Python with oldest supported libraries.
 # Does not install any optional libraries except matplotlib.
-#
-# With older python, pytest-xdist jumbles the tests differently
-# on different codes, then complains.  So we use -j 0.
 oldlibs:
   image: registry.gitlab.com/ase/ase:ase-oldlibs
-  extends: .database-configuration
-  script:
-    - pip install --no-deps .
+  before_script:
+    - pwd
+    - whoami
     - >
-      ase test -j0 --pytest --color=yes
-      -W "ignore:can't resolve package"
-      -W ignore::PendingDeprecationWarning
+      pip install
+      numpy==1.19.5 scipy==1.6.0 matplotlib==3.3.4
+      pytest==7.0.0 attrs==20.3.0 pluggy==1.0.0 py==1.11.0 six==1.15.0
+      pytest-xdist==2.1.0 execnet==1.8.1 pytest-forked==1.4.0
+    - pip install --no-deps .
+  script:
+    - ase test
+  cache:
+    key: oldlibs
+    paths:
+     - $PIP_CACHE_DIR
 
 
 # For testing newest versions of libraries against standard images
 # on dockerhub.
 pipinstall:
   image: python:3.11
-  extends: .database-configuration
-  script:
+  before_script:
     - python --version
-    - pip install psycopg2-binary pymysql cryptography
     - pip install .[test]
-    - ase test --pytest --color=yes
+  script:
+    - ase test
   when: manual
 
 # This is the main test job using new versions of libraries.
@@ -63,39 +61,18 @@ pipinstall:
 # It would be better to install it for real, and for things to just work.
 main:
   image: registry.gitlab.com/ase/ase:ase-main
-  extends: .database-configuration
-  # Inheriting variables from the database-configuration job
-  # seems to be broken all of a sudden (gitlab-runner 13.0.1 and 13.3.1)
-  # We need to redefine them here then, otherwise gitlab-runner will fail
-  # those tests when run locally.  Meanwhile everything works on gitlab.com.
-  # Strange!
-  variables:
-    POSTGRES_DB: testase
-    POSTGRES_USER: ase
-    POSTGRES_PASSWORD: "ase"
-    MYSQL_DATABASE: testase_mysql
-    MYSQL_ROOT_PASSWORD: ase
-    # MYSQL_TCP_PORT: 3306
-
-  services:
-    - postgres:latest
-    - mysql:latest
-    - mariadb:latest
-
-  # We ignore a DeprecationWarning about --rsyncdir from pytest-xdist.
-  # This seems to be internal between pytest libs.
-  script:
+  before_script:
     - python --version
     - pip install --no-deps --editable .
     - ase info --calculators
     - cd $CI_PROJECT_DIR
+  script:
     - >
       ase test
       --calculators asap,ff,lj,morse,tip3p,tip4p
-      --coverage --pytest --color=yes --durations 20 -v -r s
+      --coverage --pytest
       --junit-xml=report.xml
-      -W "ignore:The --rsyncdir command line argument"
-      -W "ignore:NumPy will stop allowing conversion of out-of-bound Python integers"
+  after_script:
     - coverage xml --data-file ase/test/.coverage
     - mv ase/test/coverage-html coverage-main
     - mv ase/test/.coverage coverage-main/coverage.dat
@@ -108,21 +85,26 @@ main:
         path: coverage.xml
       junit: ase/test/report.xml
     expire_in: 1 week
+  cache:
+    key: main
+    paths:
+     - $PIP_CACHE_DIR
 
 # Calculator integration tests which always run.
 # Encompasses those tests marked as @pytest.mark.calculator_lite.
 # Please make sure these tests are cheap.
 calculators-lite:
   image: registry.gitlab.com/ase/ase:ase-full-monty
-  script:
+  before_script:
     - pip install --no-deps --editable .
     - export ASE_CONFIG_PATH=/home/ase/aseconfig.ini
     - sed 's/binary = /command = /' -i /home/ase/aseconfig.ini
+  script:
     - >
       ase test calculator --calculators=auto --coverage
-      --pytest -m calculator_lite --color=yes --durations=20 -v
-      --junit-xml=report.xml -r s
-      -W "ignore:The --rsyncdir command line argument"
+      --pytest -m calculator_lite
+      --junit-xml=report.xml
+  after_script:
     - coverage xml --data-file ase/test/.coverage
     - mv ase/test/coverage-html coverage-calculators-lite
     - mv ase/test/.coverage coverage-calculators-lite/coverage.dat
@@ -135,6 +117,10 @@ calculators-lite:
         path: coverage.xml
       junit: ase/test/report.xml
     expire_in: 1 week
+  cache:
+    key: calculators
+    paths:
+     - $PIP_CACHE_DIR
 
 # Plan: Test as many calculators as possible as well as possible.
 # Obviously this is kind of expensive so the job is manually activated.
@@ -144,15 +130,16 @@ calculators-lite:
 # It would be great if someone could enable more calculators with this.
 calculators:
   image: registry.gitlab.com/ase/ase:ase-full-monty
-  script:
+  before_script:
     - pip install --no-deps --editable .
     - export ASE_CONFIG_PATH=/home/ase/aseconfig.ini
     - sed 's/binary = /command = /' -i /home/ase/aseconfig.ini
     - ase info --calculators
+  script:
     - >
       ase test calculator --calculators abinit,asap,cp2k,dftb,espresso,gpaw,kim,lammpslib,lammpsrun,mopac,nwchem,octopus,siesta
-      --coverage --pytest --color=yes --durations 20 --junit-xml=report.xml
-      -W "ignore:The --rsyncdir command line argument"
+      --coverage --pytest --junit-xml=report.xml
+  after_script:
     - coverage xml --data-file ase/test/.coverage
     - mv ase/test/coverage-html coverage-calculators
     - mv ase/test/.coverage coverage-calculators/coverage.dat
@@ -170,58 +157,74 @@ calculators:
         path: coverage.xml
       junit: ase/test/report.xml
     expire_in: 1 week
+  cache:
+    key: calculators
+    policy: pull
+    paths:
+     - $PIP_CACHE_DIR
 
 bleeding-edge:
-  image: python:3.12.4-alpine
+  image: python:3-alpine
   variables:
     PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
   before_script:
+    - pip install --upgrade numpy scipy matplotlib
     - pip install --editable .[test]
     - pip freeze
   script:
-    - >
-      ase test -j0 --pytest --color=yes -m 'not slow'
-      --ignore gui/ --ignore test_imports.py
-      --durations 20
+    - ase test --pytest --ignore gui/ --ignore test_imports.py
   cache:
     paths:
       - $PIP_CACHE_DIR
-    key: $CI_PROJECT_ID
+    key: bleeding-edge
 
 
 doc:
   image: registry.gitlab.com/ase/ase:ase-main
-  script:
+  variables:
+    PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache-doc"
+  before_script:
+    - pip install scriv sphinx_book_theme
     - pip install --no-deps .[docs]
     - ase info
     - which sphinx-build
-    - cd $CI_PROJECT_DIR/doc
+  script:
+    - scriv collect --keep || echo Not compiling changelog then  # Avoid error
     - python -m ase.utils.sphinx run # test scripts
-    - sphinx-build -W . build
+    - sphinx-build -W doc doc/build
+    - mv doc/build web-html
+  cache:
+    paths:
+      - $PIP_CACHE_DIR
+    key: doc
   artifacts:
     paths:
-      - $CI_PROJECT_DIR/doc/build/
+      - web-html
     expire_in: 1 week
 
 distribution-package:
   image: registry.gitlab.com/ase/ase:ase-main
-  extends: .database-configuration
   before_script:
     - mkdir dist
-    - pip install setuptools
+    - pip install setuptools build
   script:
-    - python setup.py sdist | tee dist/setup_sdist.log
-    - python setup.py bdist_wheel | tee dist/setup_bdist_wheel.log
+    - python -m build
+    - ls dist/*
     - pip install dist/ase-*.tar.gz
-    - ase test --pytest --color=yes
+    - ase test
     - pip uninstall --yes ase
     - pip install dist/ase-*-py3-none-any.whl
-    - ase test --pytest --color=yes
+    - ase test
   artifacts:
     paths:
       - dist
     expire_in: 1 week
-  when: manual
+  rules:
+    - if: $CI_COMMIT_TAG
+    - if: $CI_PIPELINE_SOURCE == "schedule"
+    - if: $CI_PIPELINE_SOURCE == "push"
+      when: manual
+
 
 # Publish code coverage data on web.
 #  * The deploy stage is specially recognized by gitlab
@@ -232,8 +235,13 @@ pages:
   stage: deploy
   dependencies:
     - coverage-combine
+    - doc
   script:
-    - mv coverage-html public
+    - mkdir public
+    - mv coverage-html public/
+    - pwd
+    - ls
+    - mv web-html/* public/
   artifacts:
     paths:
       - public
@@ -242,13 +250,22 @@ pages:
     - master
 
 lint:
-  image: registry.gitlab.com/ase/ase:ase-paperwork
+  image: python:3.12.4-alpine
+  before_script:
+    - pip install .[lint]
+    - pip freeze
   script:
-    - cd $CI_PROJECT_DIR
     - mypy --version
     - mypy --color-output -p ase
     - python -We:invalid -m compileall -f -q ase/
     - ruff check
+    - ruff format --check
+  variables:
+    PIP_CACHE_DIR: "$CI_PROJECT_DIR/pip-cache"
+  cache:
+    paths:
+      - $PIP_CACHE_DIR
+    key: lint
 
 coverage-combine:
   image: registry.gitlab.com/ase/ase:ase-paperwork
@@ -281,13 +298,13 @@ coverage-combine:
     - refreshenv
     - python --version
     - python -m pip install --upgrade pip
-    - python -m pip install pytest pytest-mock
+    - python -m pip install pytest
     - python -m pip install .
   script:
     - >
       ase test
       --calculators eam,ff,lj,morse,tip3p,tip4p
-      --pytest --color=yes --durations 20
+      --pytest
       -W "ignore:The --rsyncdir command line argument"
       -W "ignore:NumPy will stop allowing conversion of out-of-bound Python integers"
   rules:
diff -pruN 3.24.0-1/CHANGELOG.rst 3.26.0-1/CHANGELOG.rst
--- 3.24.0-1/CHANGELOG.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/CHANGELOG.rst	2025-08-12 11:26:23.000000000 +0000
@@ -3,4 +3,4 @@ Changelog
 
 See what's new in ASE here:
     
-    https://wiki.fysik.dtu.dk/ase/releasenotes.html
+    https://ase-lib.org/releasenotes.html
diff -pruN 3.24.0-1/CONTRIBUTING.rst 3.26.0-1/CONTRIBUTING.rst
--- 3.24.0-1/CONTRIBUTING.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/CONTRIBUTING.rst	2025-08-12 11:26:23.000000000 +0000
@@ -3,4 +3,4 @@ Contributing
 
 See how to contribute here:
     
-    https://wiki.fysik.dtu.dk/ase/development/contribute.html
+    https://ase-lib.org/development/contribute.html
diff -pruN 3.24.0-1/README.rst 3.26.0-1/README.rst
--- 3.24.0-1/README.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/README.rst	2025-08-12 11:26:23.000000000 +0000
@@ -4,7 +4,7 @@ Atomic Simulation Environment
 ASE is a set of tools and Python modules for setting up, manipulating,
 running, visualizing and analyzing atomistic simulations.
 
-Webpage: http://wiki.fysik.dtu.dk/ase
+Webpage: https://ase-lib.org/
 
 
 Requirements
diff -pruN 3.24.0-1/appveyor.yml 3.26.0-1/appveyor.yml
--- 3.24.0-1/appveyor.yml	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/appveyor.yml	1970-01-01 00:00:00.000000000 +0000
@@ -1,63 +0,0 @@
-environment:
-  matrix:
-    # For Python versions available on Appveyor, see
-    # http://www.appveyor.com/docs/installed-software#python
-    #  # Python 3.9
-    #- PYTHON: "C:\\Python39"
-    # Python 3.9 - 64-bit
-    - PYTHON: "C:\\Python39-x64"
-    #  # Conda 3.9
-    #- PYTHON: "C:\\Miniconda39"
-    #  # Conda 3.9 64-bit
-    #- PYTHON: "C:\\Miniconda39-x64"
-
-install:
-  # Prepend chosen Python to the PATH of this build
-  - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
-  # Check that we have the expected version and architecture for Python
-  - "python --version"
-  - "python -c \"import struct; print(struct.calcsize('P') * 8)\""
-  # Install the conda supplied packages if using conda, otherwise use pip
-  # The wheel package is needed for 'pip wheel'
-  # Turn off progressbars '-q' otherwise PowerShell thinks there are errors
-  - "echo %PYTHON%"
-  - ps: |
-      if($env:PYTHON -match "conda")
-      {
-        echo "install with conda"
-        conda update -yq conda
-        conda install -yq pip=21.0.1 wheel numpy scipy pyflakes matplotlib flask pytest pytest-mock
-      }
-      else
-      {
-        echo "install with pip"
-        #pip install --upgrade pip
-        python.exe -m pip install --upgrade pip==21.0.1
-        pip install wheel pytest --disable-pip-version-check
-      }
-  # install ase into the current python
-  - "echo %cd%"
-  - "where pip"
-  - "pip install .[test] --disable-pip-version-check"
-
-build: off
-
-test_script:
-  # run tests from temp dir so source tree doesn't interfere
-  - "cd %TEMP%"
-  - "ase info"
-  - "ase -T test"
-
-after_test:
-  # This step builds distribution.
-  - "cd %APPVEYOR_BUILD_FOLDER%"
-  - "pip wheel -w dist --no-deps ."
-
-artifacts:
-  # bdist_wheel puts your built wheel in the dist directory
-  - path: dist\*
-
-#on_success:
-#  You can use this step to upload your artifacts to a public website.
-#  See Appveyor's documentation for more details. Or you can simply
-#  access your wheels from the Appveyor "artifacts" tab for your build.
diff -pruN 3.24.0-1/ase/_4/__init__.py 3.26.0-1/ase/_4/__init__.py
--- 3.24.0-1/ase/_4/__init__.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/_4/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,3 @@
+"""This package holds temporary developments for ASE 4.
+
+Things in ase._4 are likely to change without warning."""
diff -pruN 3.24.0-1/ase/__init__.py 3.26.0-1/ase/__init__.py
--- 3.24.0-1/ase/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -10,6 +10,6 @@ from ase.atom import Atom
 from ase.atoms import Atoms
 
 __all__ = ['Atoms', 'Atom']
-__version__ = '3.24.0'
+__version__ = '3.26.0'
 
 ase.parallel  # silence pyflakes
diff -pruN 3.24.0-1/ase/atom.py 3.26.0-1/ase/atom.py
--- 3.24.0-1/ase/atom.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/atom.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines the Atom object."""
 
 import numpy as np
diff -pruN 3.24.0-1/ase/atoms.py 3.26.0-1/ase/atoms.py
--- 3.24.0-1/ase/atoms.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/atoms.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Copyright 2008, 2009 CAMd
 # (see accompanying license files for details).
 
@@ -6,13 +8,16 @@
 This module defines the central object in the ASE package: the Atoms
 object.
 """
+from __future__ import annotations
+
 import copy
 import numbers
 from math import cos, pi, sin
+from typing import Sequence, Union, overload
 
 import numpy as np
 
-import ase.units as units
+from ase import units
 from ase.atom import Atom
 from ase.cell import Cell
 from ase.data import atomic_masses, atomic_masses_common
@@ -200,7 +205,7 @@ class Atoms:
             if info is None:
                 info = copy.deepcopy(atoms.info)
 
-        self.arrays = {}
+        self.arrays: dict[str, np.ndarray] = {}
 
         if symbols is not None and numbers is not None:
             raise TypeError('Use only one of "symbols" and "numbers".')
@@ -266,10 +271,28 @@ class Atoms:
 
     @property
     def symbols(self):
-        """Get chemical symbols as a :class:`ase.symbols.Symbols` object.
+        """Get chemical symbols as a :class:`~ase.symbols.Symbols` object.
+
+        The object works like ``atoms.numbers`` except its values are strings.
+        It supports in-place editing.
 
-        The object works like ``atoms.numbers`` except its values
-        are strings.  It supports in-place editing."""
+        Examples
+        --------
+        >>> from ase.build import molecule
+        >>> atoms = molecule('CH3CH2OH')
+        >>> atoms.symbols
+        Symbols('C2OH6')
+        >>> list(atoms.symbols)
+        ['C', 'C', 'O', 'H', 'H', 'H', 'H', 'H', 'H']
+        >>> atoms.symbols == 'C'  # doctest: +ELLIPSIS
+        array([ True,  True, False, False, False, False, False, False, False]...)
+        >>> atoms.symbols.indices()
+        {'C': array([0, 1]), 'O': array([2]), 'H': array([3, 4, 5, 6, 7, 8])}
+        >>> list(atoms.symbols.indices())  # unique elements
+        ['C', 'O', 'H']
+        >>> atoms.symbols.species()  # doctest: +SKIP
+        {'C', 'O', 'H'}
+        """  # noqa
         return Symbols(self.numbers)
 
     @symbols.setter
@@ -277,6 +300,71 @@ class Atoms:
         new_symbols = Symbols.fromsymbols(obj)
         self.numbers[:] = new_symbols.numbers
 
+    def get_chemical_symbols(self):
+        """Get list of chemical symbol strings.
+
+        Equivalent to ``list(atoms.symbols)``."""
+        return list(self.symbols)
+
+    def set_chemical_symbols(self, symbols):
+        """Set chemical symbols."""
+        self.set_array('numbers', symbols2numbers(symbols), int, ())
+
+    @property
+    def numbers(self):
+        """Attribute for direct manipulation of the atomic numbers."""
+        return self.arrays['numbers']
+
+    @numbers.setter
+    def numbers(self, numbers):
+        self.set_atomic_numbers(numbers)
+
+    def set_atomic_numbers(self, numbers):
+        """Set atomic numbers."""
+        self.set_array('numbers', numbers, int, ())
+
+    def get_atomic_numbers(self):
+        """Get integer array of atomic numbers."""
+        return self.arrays['numbers'].copy()
+
+    @property
+    def positions(self):
+        """Attribute for direct manipulation of the positions."""
+        return self.arrays['positions']
+
+    @positions.setter
+    def positions(self, pos):
+        self.arrays['positions'][:] = pos
+
+    def set_positions(self, newpositions, apply_constraint=True):
+        """Set positions, honoring any constraints. To ignore constraints,
+        use *apply_constraint=False*."""
+        if self.constraints and apply_constraint:
+            newpositions = np.array(newpositions, float)
+            for constraint in self.constraints:
+                constraint.adjust_positions(self, newpositions)
+
+        self.set_array('positions', newpositions, shape=(3,))
+
+    def get_positions(self, wrap=False, **wrap_kw):
+        """Get array of positions.
+
+        Parameters:
+
+        wrap: bool
+            wrap atoms back to the cell before returning positions
+        wrap_kw: (keyword=value) pairs
+            optional keywords `pbc`, `center`, `pretty_translation`, `eps`,
+            see :func:`ase.geometry.wrap_positions`
+        """
+        from ase.geometry import wrap_positions
+        if wrap:
+            if 'pbc' not in wrap_kw:
+                wrap_kw['pbc'] = self.pbc
+            return wrap_positions(self.positions, self.cell, **wrap_kw)
+        else:
+            return self.arrays['positions'].copy()
+
     @deprecated("Please use atoms.calc = calc", FutureWarning)
     def set_calculator(self, calc=None):
         """Attach calculator object.
@@ -330,6 +418,18 @@ class Atoms:
         """
         return self.cell.rank
 
+    @property
+    def constraints(self):
+        return self._constraints
+
+    @constraints.setter
+    def constraints(self, constraint):
+        self.set_constraint(constraint)
+
+    @constraints.deleter
+    def constraints(self):
+        self._constraints = []
+
     def set_constraint(self, constraint=None):
         """Apply one or more constrains.
 
@@ -345,15 +445,6 @@ class Atoms:
             else:
                 self._constraints = [constraint]
 
-    def _get_constraints(self):
-        return self._constraints
-
-    def _del_constraints(self):
-        self._constraints = []
-
-    constraints = property(_get_constraints, set_constraint, _del_constraints,
-                           'Constraints of the atoms.')
-
     def get_number_of_degrees_of_freedom(self):
         """Calculate the number of degrees of freedom in the system."""
         return len(self) * 3 - sum(
@@ -553,24 +644,6 @@ class Atoms:
         # XXX extend has to calculator properties
         return name in self.arrays
 
-    def set_atomic_numbers(self, numbers):
-        """Set atomic numbers."""
-        self.set_array('numbers', numbers, int, ())
-
-    def get_atomic_numbers(self):
-        """Get integer array of atomic numbers."""
-        return self.arrays['numbers'].copy()
-
-    def get_chemical_symbols(self):
-        """Get list of chemical symbol strings.
-
-        Equivalent to ``list(atoms.symbols)``."""
-        return list(self.symbols)
-
-    def set_chemical_symbols(self, symbols):
-        """Set chemical symbols."""
-        self.set_array('numbers', symbols2numbers(symbols), int, ())
-
     def get_chemical_formula(self, mode='hill', empirical=False):
         """Get the chemical formula as a string based on the chemical symbols.
 
@@ -624,10 +697,6 @@ class Atoms:
                     constraint.adjust_momenta(self, momenta)
         self.set_array('momenta', momenta, float, (3,))
 
-    def set_velocities(self, velocities):
-        """Set the momenta by specifying the velocities."""
-        self.set_momenta(self.get_masses()[:, np.newaxis] * velocities)
-
     def get_momenta(self):
         """Get array of momenta."""
         if 'momenta' in self.arrays:
@@ -635,6 +704,16 @@ class Atoms:
         else:
             return np.zeros((len(self), 3))
 
+    def get_velocities(self):
+        """Get array of velocities."""
+        momenta = self.get_momenta()
+        masses = self.get_masses()
+        return momenta / masses[:, np.newaxis]
+
+    def set_velocities(self, velocities):
+        """Set the momenta by specifying the velocities."""
+        self.set_momenta(self.get_masses()[:, np.newaxis] * velocities)
+
     def set_masses(self, masses='defaults'):
         """Set atomic masses in atomic mass units.
 
@@ -656,12 +735,27 @@ class Atoms:
                     masses[i] = atomic_masses[self.numbers[i]]
         self.set_array('masses', masses, float, ())
 
-    def get_masses(self):
-        """Get array of masses in atomic mass units."""
+    def get_masses(self) -> np.ndarray:
+        """Get masses of atoms.
+
+        Returns
+        -------
+        masses : np.ndarray
+            Atomic masses in dalton (unified atomic mass units).
+
+        Examples
+        --------
+        >>> from ase.build import molecule
+        >>> atoms = molecule('CH4')
+        >>> atoms.get_masses()
+        array([ 12.011,   1.008,   1.008,   1.008,   1.008])
+        >>> total_mass = atoms.get_masses().sum()
+        >>> print(f'{total_mass:f}')
+        16.043000
+        """
         if 'masses' in self.arrays:
             return self.arrays['masses'].copy()
-        else:
-            return atomic_masses[self.arrays['numbers']]
+        return atomic_masses[self.arrays['numbers']]
 
     def set_initial_magnetic_moments(self, magmoms=None):
         """Set the initial magnetic moments.
@@ -720,35 +814,6 @@ class Atoms:
             from ase.calculators.calculator import PropertyNotImplementedError
             raise PropertyNotImplementedError
 
-    def set_positions(self, newpositions, apply_constraint=True):
-        """Set positions, honoring any constraints. To ignore constraints,
-        use *apply_constraint=False*."""
-        if self.constraints and apply_constraint:
-            newpositions = np.array(newpositions, float)
-            for constraint in self.constraints:
-                constraint.adjust_positions(self, newpositions)
-
-        self.set_array('positions', newpositions, shape=(3,))
-
-    def get_positions(self, wrap=False, **wrap_kw):
-        """Get array of positions.
-
-        Parameters:
-
-        wrap: bool
-            wrap atoms back to the cell before returning positions
-        wrap_kw: (keyword=value) pairs
-            optional keywords `pbc`, `center`, `pretty_translation`, `eps`,
-            see :func:`ase.geometry.wrap_positions`
-        """
-        from ase.geometry import wrap_positions
-        if wrap:
-            if 'pbc' not in wrap_kw:
-                wrap_kw['pbc'] = self.pbc
-            return wrap_positions(self.positions, self.cell, **wrap_kw)
-        else:
-            return self.arrays['positions'].copy()
-
     def get_potential_energy(self, force_consistent=False,
                              apply_constraint=True):
         """Calculate potential energy.
@@ -798,12 +863,6 @@ class Atoms:
             return 0.0
         return 0.5 * np.vdot(momenta, self.get_velocities())
 
-    def get_velocities(self):
-        """Get array of velocities."""
-        momenta = self.get_momenta()
-        masses = self.get_masses()
-        return momenta / masses[:, np.newaxis]
-
     def get_total_energy(self):
         """Get the total energy - potential plus kinetic energy."""
         return self.get_potential_energy() + self.get_kinetic_energy()
@@ -1134,6 +1193,12 @@ class Atoms:
         for i in range(len(self)):
             yield self[i]
 
+    @overload
+    def __getitem__(self, i: Union[int, np.integer]) -> Atom: ...
+
+    @overload
+    def __getitem__(self, i: Union[Sequence, slice, np.ndarray]) -> Atoms: ...
+
     def __getitem__(self, i):
         """Return a subset of the atoms.
 
@@ -1539,53 +1604,42 @@ class Atoms:
             center = np.array(center, float)
         return center
 
-    def euler_rotate(self, phi=0.0, theta=0.0, psi=0.0, center=(0, 0, 0)):
+    def euler_rotate(
+        self,
+        phi: float = 0.0,
+        theta: float = 0.0,
+        psi: float = 0.0,
+        center: Sequence[float] = (0.0, 0.0, 0.0),
+    ) -> None:
         """Rotate atoms via Euler angles (in degrees).
 
         See e.g http://mathworld.wolfram.com/EulerAngles.html for explanation.
 
-        Parameters:
+        Note that the rotations in this method are passive and applied **not**
+        to the atomic coordinates in the present frame **but** the frame itself.
 
-        center :
-            The point to rotate about. A sequence of length 3 with the
-            coordinates, or 'COM' to select the center of mass, 'COP' to
-            select center of positions or 'COU' to select center of cell.
-        phi :
+        Parameters
+        ----------
+        phi : float
             The 1st rotation angle around the z axis.
-        theta :
+        theta : float
             Rotation around the x axis.
-        psi :
+        psi : float
             2nd rotation around the z axis.
+        center : Sequence[float], default = (0.0, 0.0, 0.0)
+            The point to rotate about. A sequence of length 3 with the
+            coordinates, or 'COM' to select the center of mass, 'COP' to
+            select center of positions or 'COU' to select center of cell.
 
         """
+        from scipy.spatial.transform import Rotation as R
+
         center = self._centering_as_array(center)
 
-        phi *= pi / 180
-        theta *= pi / 180
-        psi *= pi / 180
-
-        # First move the molecule to the origin In contrast to MATLAB,
-        # numpy broadcasts the smaller array to the larger row-wise,
-        # so there is no need to play with the Kronecker product.
-        rcoords = self.positions - center
-        # First Euler rotation about z in matrix form
-        D = np.array(((cos(phi), sin(phi), 0.),
-                      (-sin(phi), cos(phi), 0.),
-                      (0., 0., 1.)))
-        # Second Euler rotation about x:
-        C = np.array(((1., 0., 0.),
-                      (0., cos(theta), sin(theta)),
-                      (0., -sin(theta), cos(theta))))
-        # Third Euler rotation, 2nd rotation about z:
-        B = np.array(((cos(psi), sin(psi), 0.),
-                      (-sin(psi), cos(psi), 0.),
-                      (0., 0., 1.)))
-        # Total Euler rotation
-        A = np.dot(B, np.dot(C, D))
-        # Do the rotation
-        rcoords = np.dot(A, np.transpose(rcoords))
-        # Move back to the rotation point
-        self.positions = np.transpose(rcoords) + center
+        # passive rotations (negative angles) for backward compatibility
+        rotation = R.from_euler('zxz', (-phi, -theta, -psi), degrees=True)
+
+        self.positions = rotation.apply(self.positions - center) + center
 
     def get_dihedral(self, a0, a1, a2, a3, mic=False):
         """Calculate dihedral angle.
@@ -2005,27 +2059,6 @@ class Atoms:
                 .format(self.cell.rank))
         return self.cell.volume
 
-    def _get_positions(self):
-        """Return reference to positions-array for in-place manipulations."""
-        return self.arrays['positions']
-
-    def _set_positions(self, pos):
-        """Set positions directly, bypassing constraints."""
-        self.arrays['positions'][:] = pos
-
-    positions = property(_get_positions, _set_positions,
-                         doc='Attribute for direct ' +
-                         'manipulation of the positions.')
-
-    def _get_atomic_numbers(self):
-        """Return reference to atomic numbers for in-place
-        manipulations."""
-        return self.arrays['numbers']
-
-    numbers = property(_get_atomic_numbers, set_atomic_numbers,
-                       doc='Attribute for direct ' +
-                       'manipulation of the atomic numbers.')
-
     @property
     def cell(self):
         """The :class:`ase.cell.Cell` for direct manipulation."""
diff -pruN 3.24.0-1/ase/build/__init__.py 3.26.0-1/ase/build/__init__.py
--- 3.24.0-1/ase/build/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.build.bulk import bulk
 from ase.build.connected import (
     connected_atoms,
diff -pruN 3.24.0-1/ase/build/attach.py 3.26.0-1/ase/build/attach.py
--- 3.24.0-1/ase/build/attach.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/attach.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.geometry import get_distances
diff -pruN 3.24.0-1/ase/build/bulk.py 3.26.0-1/ase/build/bulk.py
--- 3.24.0-1/ase/build/bulk.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/bulk.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Build crystalline systems"""
 from math import sqrt
 from typing import Any
diff -pruN 3.24.0-1/ase/build/general_surface.py 3.26.0-1/ase/build/general_surface.py
--- 3.24.0-1/ase/build/general_surface.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/general_surface.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from math import gcd
 
 import numpy as np
diff -pruN 3.24.0-1/ase/build/molecule.py 3.26.0-1/ase/build/molecule.py
--- 3.24.0-1/ase/build/molecule.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/molecule.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.atoms import Atoms
 from ase.collections import g2
 
diff -pruN 3.24.0-1/ase/build/niggli.py 3.26.0-1/ase/build/niggli.py
--- 3.24.0-1/ase/build/niggli.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/niggli.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 
diff -pruN 3.24.0-1/ase/build/ribbon.py 3.26.0-1/ase/build/ribbon.py
--- 3.24.0-1/ase/build/ribbon.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/ribbon.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from math import sqrt
 
 import numpy as np
diff -pruN 3.24.0-1/ase/build/root.py 3.26.0-1/ase/build/root.py
--- 3.24.0-1/ase/build/root.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/root.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from math import atan2, cos, log10, sin
 
 import numpy as np
diff -pruN 3.24.0-1/ase/build/rotate.py 3.26.0-1/ase/build/rotate.py
--- 3.24.0-1/ase/build/rotate.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/rotate.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.geometry import find_mic
diff -pruN 3.24.0-1/ase/build/supercells.py 3.26.0-1/ase/build/supercells.py
--- 3.24.0-1/ase/build/supercells.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/supercells.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,17 +1,72 @@
-"""Helper functions for creating supercells."""
+# fmt: off
 
-import warnings
+"""Helper functions for creating supercells."""
 
 import numpy as np
 
 from ase import Atoms
+from ase.utils import deprecated
 
 
 class SupercellError(Exception):
     """Use if construction of supercell fails"""
 
 
-def get_deviation_from_optimal_cell_shape(cell, target_shape="sc", norm=None):
+@deprecated('use `eval_length_deviation` instead.')
+def get_deviation_from_optimal_cell_shape(*args, **kwargs):
+    return eval_length_deviation(*args, **kwargs)
+
+
+def eval_shape_deviation(cell, target_shape="sc"):
+    r"""
+    Calculates the deviation of the given cell from the target cell metric.
+
+    Parameters
+    ----------
+    cell : (..., 3, 3) array_like
+        Metric given as a 3x3 matrix of the input structure.
+        Multiple cells can also be given as a higher-dimensional array.
+    target_shape : {'sc', 'fcc'}
+        Desired supercell shape.
+
+    Returns
+    -------
+    float or ndarray
+        Cell metric(s) (0 is perfect score)
+
+    """
+
+    cell = np.asarray(cell)
+
+    eff_cubic_length = np.cbrt(np.abs(np.linalg.det(cell)))  # 'a_0'
+
+    if target_shape == 'sc':
+        target_len = eff_cubic_length
+        target_cos = 0.0  # cos(+-pi/2) = 0.0
+        target_metric = np.eye(3)
+    elif target_shape == 'fcc':
+        # FCC is characterised by 60 degree angles & lattice vectors = 2**(1/6)
+        # times the eff cubic length:
+        target_len = eff_cubic_length * 2 ** (1 / 6)
+        target_cos = 0.5  # cos(+-pi/3) = 0.5
+        target_metric = np.eye(3) + target_cos * (np.ones((3, 3)) - np.eye(3))
+    else:
+        raise ValueError(target_shape)
+
+    # calculate cell @ cell.T for (... , 3, 3)
+    # with cell  -> C_mij
+    # and metric -> M_mkl
+    # M_mkl = (sum_j C_mkj * C_mlj) / leff**2
+    metric = cell @ np.swapaxes(cell, -2, -1)
+    normed = metric / target_len[..., None, None] ** 2
+
+    # offdiagonal ~ cos angle -> score = np.abs(cos angle - cos target_angle)
+    scores = np.add.reduce((normed - target_metric) ** 2, axis=(-2, -1))
+
+    return scores
+
+
+def eval_length_deviation(cell, target_shape="sc"):
     r"""Calculate the deviation from the target cell shape.
 
     Calculates the deviation of the given cell metric from the ideal
@@ -35,10 +90,6 @@ def get_deviation_from_optimal_cell_shap
     target_shape : {'sc', 'fcc'}
         Desired supercell shape. Can be 'sc' for simple cubic or
         'fcc' for face-centered cubic.
-    norm : float
-        Specify the normalization factor. This is useful to avoid
-        recomputing the normalization factor when computing the
-        deviation for a series of P matrices.
 
     Returns
     -------
@@ -49,76 +100,34 @@ def get_deviation_from_optimal_cell_shap
         `norm` is unused in ASE 3.24.0 and removed in ASE 3.25.0.
 
     """
-    if norm is not None:
-        warnings.warn(
-            '`norm` is unused in ASE 3.24.0 and removed in ASE 3.25.0',
-            FutureWarning,
-        )
 
     cell = np.asarray(cell)
     cell_lengths = np.sqrt(np.add.reduce(cell**2, axis=-1))
+
     eff_cubic_length = np.cbrt(np.abs(np.linalg.det(cell)))  # 'a_0'
 
     if target_shape == 'sc':
-        target_length = eff_cubic_length
+        target_len = eff_cubic_length
 
     elif target_shape == 'fcc':
         # FCC is characterised by 60 degree angles & lattice vectors = 2**(1/6)
         # times the eff cubic length:
-        target_length = eff_cubic_length * 2 ** (1 / 6)
+        target_len = eff_cubic_length * 2 ** (1 / 6)
 
     else:
         raise ValueError(target_shape)
 
-    inv_target_length = 1.0 / target_length
+    inv_target_len = 1.0 / target_len
 
     # rms difference to eff cubic/FCC length:
-    diffs = cell_lengths * inv_target_length[..., None] - 1.0
-    return np.sqrt(np.add.reduce(diffs**2, axis=-1))
-
+    diffs = cell_lengths * inv_target_len[..., None] - 1.0
+    scores = np.sqrt(np.add.reduce(diffs**2, axis=-1))
 
-def find_optimal_cell_shape(
-    cell,
-    target_size,
-    target_shape,
-    lower_limit=-2,
-    upper_limit=2,
-    verbose=False,
-):
-    """Obtain the optimal transformation matrix for a supercell of target size
-    and shape.
+    return scores
 
-    Returns the transformation matrix that produces a supercell
-    corresponding to *target_size* unit cells with metric *cell* that
-    most closely approximates the shape defined by *target_shape*.
 
-    Updated with code from the `doped` defect simulation package
-    (https://doped.readthedocs.io) to be rotationally invariant and
-    allow transformation matrices with negative determinants, boosting
-    performance.
-
-    Parameters:
-
-    cell: 2D array of floats
-        Metric given as a (3x3 matrix) of the input structure.
-    target_size: integer
-        Size of desired supercell in number of unit cells.
-    target_shape: str
-        Desired supercell shape. Can be 'sc' for simple cubic or
-        'fcc' for face-centered cubic.
-    lower_limit: int
-        Lower limit of search range.
-    upper_limit: int
-        Upper limit of search range.
-    verbose: bool
-        Set to True to obtain additional information regarding
-        construction of transformation matrix.
-
-    Returns:
-        2D array of integers: Transformation matrix that produces the
-        optimal supercell.
-    """
-    cell = np.asarray(cell)
+def _guess_initial_transformation(cell, target_shape,
+                                  target_size, verbose=False):
 
     # Set up target metric
     if target_shape == 'sc':
@@ -150,41 +159,140 @@ def find_optimal_cell_shape(
         print("closest integer transformation matrix (P_0):")
         print(starting_P)
 
+    return ideal_P, starting_P
+
+
+def _build_matrix_operations(starting_P, lower_limit, upper_limit):
+    mat_dim = starting_P.shape[0]
+
+    if not mat_dim == starting_P.shape[1]:
+        raise ValueError('Cell matrix should be quadratic.')
+
     # Build a big matrix of all admissible integer matrix operations.
     # (If this takes too much memory we could do blocking but there are
     # too many for looping one by one.)
-    dimensions = [(upper_limit + 1) - lower_limit] * 9
-    operations = np.moveaxis(np.indices(dimensions), 0, -1).reshape(-1, 3, 3)
+    dimensions = [(upper_limit + 1) - lower_limit] * mat_dim**2
+    operations = np.moveaxis(np.indices(dimensions), 0, -1)
+    operations = operations.reshape(-1, mat_dim, mat_dim)
     operations += lower_limit  # Each element runs from lower to upper limits.
     operations += starting_P
-    determinants = np.linalg.det(operations)
+
+    return operations
+
+
+def _screen_supercell_size(operations, target_size):
 
     # screen supercells with the target size
-    good_indices = np.where(abs(determinants - target_size) < 1e-12)[0]
+    determinants = np.round(np.linalg.det(operations), 0).astype(int)
+    good_indices = np.where(np.abs(determinants) == target_size)[0]
+
     if not good_indices.size:
         print("Failed to find a transformation matrix.")
         return None
     operations = operations[good_indices]
 
-    # evaluate derivations of the screened supercells
-    scores = get_deviation_from_optimal_cell_shape(
-        operations @ cell,
-        target_shape,
-    )
+    return operations
+
+
+def _optimal_transformation(operations, scores, ideal_P):
+
     imin = np.argmin(scores)
     best_score = scores[imin]
-
     # screen candidates with the same best score
     operations = operations[np.abs(scores - best_score) < 1e-6]
 
     # select the one whose cell orientation is the closest to the target
     # https://gitlab.com/ase/ase/-/merge_requests/3522
     imin = np.argmin(np.add.reduce((operations - ideal_P)**2, axis=(-2, -1)))
+
     optimal_P = operations[imin]
 
     if np.linalg.det(optimal_P) <= 0:
         optimal_P *= -1  # flip signs if negative determinant
 
+    return optimal_P, best_score
+
+
+all_score_funcs = {"length": eval_length_deviation,
+                   "metric": eval_shape_deviation}
+
+
+def find_optimal_cell_shape(
+    cell,
+    target_size,
+    target_shape,
+    lower_limit=-2,
+    upper_limit=2,
+    verbose=False,
+    score_key='length'
+):
+    """Obtain the optimal transformation matrix for a supercell of target size
+    and shape.
+
+    Returns the transformation matrix that produces a supercell
+    corresponding to *target_size* unit cells with metric *cell* that
+    most closely approximates the shape defined by *target_shape*.
+
+    Updated with code from the `doped` defect simulation package
+    (https://doped.readthedocs.io) to be rotationally invariant and
+    allow transformation matrices with negative determinants, boosting
+    performance.
+
+    Parameters:
+
+    cell: 2D array of floats
+        Metric given as a (3x3 matrix) of the input structure.
+    target_size: integer
+        Size of desired supercell in number of unit cells.
+    target_shape: str
+        Desired supercell shape. Can be 'sc' for simple cubic or
+        'fcc' for face-centered cubic.
+    lower_limit: int
+        Lower limit of search range.
+    upper_limit: int
+        Upper limit of search range.
+    verbose: bool
+        Set to True to obtain additional information regarding
+        construction of transformation matrix.
+    score_key: str
+        key from all_score_funcs to select score function.
+
+    Returns:
+        2D array of integers: Transformation matrix that produces the
+        optimal supercell.
+    """
+
+    # transform to np.array
+    cell = np.asarray(cell)
+
+    # get starting transformation
+    # ideal_P ... transformation: target_cell = ideal_P @ cell
+    # starting_P ... integer rounded (ideal_P)
+    ideal_P, starting_P = _guess_initial_transformation(cell, target_shape,
+                                                        target_size,
+                                                        verbose=verbose)
+
+    # build all admissible matrix operations 'centered' at starting_P
+    operations = _build_matrix_operations(starting_P,
+                                          lower_limit, upper_limit)
+
+    # pre-screen operations based on target_size
+    operations = _screen_supercell_size(operations, target_size)
+
+    # evaluate derivations of the screened supercells
+    if score_key in all_score_funcs:
+        get_deviation_score = all_score_funcs[score_key]
+    else:
+        msg = (f'Score func key {score_key} not implemented.'
+               + f'Please select from {all_score_funcs}.')
+        raise SupercellError(msg)
+
+    scores = get_deviation_score(operations @ cell,
+                                 target_shape)
+
+    # obtain optimal transformation from scores
+    optimal_P, best_score = _optimal_transformation(operations, scores, ideal_P)
+
     # Finalize.
     if verbose:
         print(f"smallest score (|Q P h_p - h_target|_2): {best_score:f}")
@@ -194,6 +302,7 @@ def find_optimal_cell_shape(
         print(np.round(np.dot(optimal_P, cell), 4))
         det = np.linalg.det(optimal_P)
         print(f"determinant of optimal transformation matrix: {det:g}")
+
     return optimal_P
 
 
diff -pruN 3.24.0-1/ase/build/surface.py 3.26.0-1/ase/build/surface.py
--- 3.24.0-1/ase/build/surface.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/surface.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Helper functions for creating the most common surfaces and related tasks.
 
 The helper functions can create the most common low-index surfaces,
diff -pruN 3.24.0-1/ase/build/surfaces_with_termination.py 3.26.0-1/ase/build/surfaces_with_termination.py
--- 3.24.0-1/ase/build/surfaces_with_termination.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/surfaces_with_termination.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.build.general_surface import surface
diff -pruN 3.24.0-1/ase/build/tools.py 3.26.0-1/ase/build/tools.py
--- 3.24.0-1/ase/build/tools.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/tools.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.build.niggli import niggli_reduce_cell
diff -pruN 3.24.0-1/ase/build/tube.py 3.26.0-1/ase/build/tube.py
--- 3.24.0-1/ase/build/tube.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/build/tube.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from math import gcd, sqrt
 
 import numpy as np
diff -pruN 3.24.0-1/ase/calculators/abc.py 3.26.0-1/ase/calculators/abc.py
--- 3.24.0-1/ase/calculators/abc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/abc.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 This module defines abstract helper classes with the objective of reducing
 boilerplace method definitions (i.e. duplication) in calculators.
diff -pruN 3.24.0-1/ase/calculators/abinit.py 3.26.0-1/ase/calculators/abinit.py
--- 3.24.0-1/ase/calculators/abinit.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/abinit.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines an ASE interface to ABINIT.
 
 http://www.abinit.org/
diff -pruN 3.24.0-1/ase/calculators/acemolecule.py 3.26.0-1/ase/calculators/acemolecule.py
--- 3.24.0-1/ase/calculators/acemolecule.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/acemolecule.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 from copy import deepcopy
 
diff -pruN 3.24.0-1/ase/calculators/acn.py 3.26.0-1/ase/calculators/acn.py
--- 3.24.0-1/ase/calculators/acn.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/acn.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 import ase.units as units
diff -pruN 3.24.0-1/ase/calculators/aims.py 3.26.0-1/ase/calculators/aims.py
--- 3.24.0-1/ase/calculators/aims.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/aims.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines an ASE interface to FHI-aims.
 
 Felix Hanke hanke@liverpool.ac.uk
@@ -158,8 +160,7 @@ class AimsTemplate(CalculatorTemplate):
         write_control(control, atoms, parameters)
 
     def execute(self, directory, profile):
-        profile.run(directory, None, self.outputname,
-                    errorfile=self.errorname)
+        profile.run(directory, None, self.outputname, errorfile=self.errorname)
 
     def read_results(self, directory):
         from ase.io.aims import read_aims_results
@@ -171,7 +172,7 @@ class AimsTemplate(CalculatorTemplate):
         return AimsProfile.from_config(cfg, self.name, **kwargs)
 
     def socketio_argv(self, profile, unixsocket, port):
-        return [profile.command]
+        return profile._split_command
 
     def socketio_parameters(self, unixsocket, port):
         if port:
diff -pruN 3.24.0-1/ase/calculators/amber.py 3.26.0-1/ase/calculators/amber.py
--- 3.24.0-1/ase/calculators/amber.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/amber.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines an ASE interface to Amber16.
 
 Usage: (Tested only with Amber16, http://ambermd.org/)
diff -pruN 3.24.0-1/ase/calculators/bond_polarizability.py 3.26.0-1/ase/calculators/bond_polarizability.py
--- 3.24.0-1/ase/calculators/bond_polarizability.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/bond_polarizability.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import Tuple
 
 import numpy as np
diff -pruN 3.24.0-1/ase/calculators/calculator.py 3.26.0-1/ase/calculators/calculator.py
--- 3.24.0-1/ase/calculators/calculator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/calculator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import copy
 import os
 import shlex
@@ -203,6 +205,10 @@ def get_calculator_class(name):
         from ase.calculators.acemolecule import ACE as Calculator
     elif name == 'Psi4':
         from ase.calculators.psi4 import Psi4 as Calculator
+    elif name == 'mattersim':
+        from mattersim.forcefield import MatterSimCalculator as Calculator
+    elif name == 'mace_mp':
+        from mace.calculators import mace_mp as Calculator
     elif name in external_calculators:
         Calculator = external_calculators[name]
     else:
@@ -387,31 +393,6 @@ def kpts2ndarray(kpts, atoms=None):
     return kpts2kpts(kpts, atoms=atoms).kpts
 
 
-class EigenvalOccupationMixin:
-    """Define 'eigenvalues' and 'occupations' properties on class.
-
-    eigenvalues and occupations will be arrays of shape (spin, kpts, nbands).
-
-    Classes must implement the old-fashioned get_eigenvalues and
-    get_occupations methods."""
-
-    # We should maybe deprecate this and rely on the new
-    # Properties object for eigenvalues/occupations.
-
-    @property
-    def eigenvalues(self):
-        return self._propwrapper().eigenvalues
-
-    @property
-    def occupations(self):
-        return self._propwrapper().occupations
-
-    def _propwrapper(self):
-        from ase.calculator.singlepoint import OutputPropertyWrapper
-
-        return OutputPropertyWrapper(self)
-
-
 class Parameters(dict):
     """Dictionary for parameters.
 
@@ -569,6 +550,10 @@ class BaseCalculator(GetPropertiesMixin)
     def name(self) -> str:
         return self._get_name()
 
+    def todict(self) -> dict[str, Any]:
+        """Obtain a dictionary of parameter information"""
+        return {}
+
 
 class Calculator(BaseCalculator):
     """Base-class for all ASE calculators.
diff -pruN 3.24.0-1/ase/calculators/castep.py 3.26.0-1/ase/calculators/castep.py
--- 3.24.0-1/ase/calculators/castep.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/castep.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,5 +1,7 @@
+# fmt: off
+
 """This module defines an interface to CASTEP for
-    use by the ASE (Webpage: http://wiki.fysik.dtu.dk/ase)
+    use by the ASE (Webpage: https://ase-lib.org/)
 
 Authors:
     Max Hoffmann, max.hoffmann@ch.tum.de
diff -pruN 3.24.0-1/ase/calculators/checkpoint.py 3.26.0-1/ase/calculators/checkpoint.py
--- 3.24.0-1/ase/calculators/checkpoint.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/checkpoint.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Checkpointing and restart functionality for scripts using ASE Atoms objects.
 
 Initialize checkpoint object:
@@ -268,11 +270,9 @@ class CheckpointCalculator(Calculator):
         try:
             results = self.checkpoint.load(atoms)
             prev_atoms, results = results[0], results[1:]
-            try:
-                assert atoms_almost_equal(atoms, prev_atoms)
-            except AssertionError:
-                raise AssertionError('mismatch between current atoms and '
-                                     'those read from checkpoint file')
+            if not atoms_almost_equal(atoms, prev_atoms):
+                raise RuntimeError('mismatch between current atoms and '
+                                   'those read from checkpoint file')
             self.logfile.write('retrieved results for {} from checkpoint\n'
                                .format(properties))
             # save results in calculator for next time
diff -pruN 3.24.0-1/ase/calculators/combine_mm.py 3.26.0-1/ase/calculators/combine_mm.py
--- 3.24.0-1/ase/calculators/combine_mm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/combine_mm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import copy
 
 import numpy as np
diff -pruN 3.24.0-1/ase/calculators/counterions.py 3.26.0-1/ase/calculators/counterions.py
--- 3.24.0-1/ase/calculators/counterions.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/counterions.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase import units
diff -pruN 3.24.0-1/ase/calculators/cp2k.py 3.26.0-1/ase/calculators/cp2k.py
--- 3.24.0-1/ase/calculators/cp2k.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/cp2k.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines an ASE interface to CP2K.
 
 https://www.cp2k.org/
diff -pruN 3.24.0-1/ase/calculators/crystal.py 3.26.0-1/ase/calculators/crystal.py
--- 3.24.0-1/ase/calculators/crystal.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/crystal.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines an ASE interface to CRYSTAL14/CRYSTAL17
 
 http://www.crystal.unito.it/
diff -pruN 3.24.0-1/ase/calculators/demon/demon.py 3.26.0-1/ase/calculators/demon/demon.py
--- 3.24.0-1/ase/calculators/demon/demon.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/demon/demon.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines an ASE interface to deMon.
 
 http://www.demon-software.com
diff -pruN 3.24.0-1/ase/calculators/demon/demon_io.py 3.26.0-1/ase/calculators/demon/demon_io.py
--- 3.24.0-1/ase/calculators/demon/demon_io.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/demon/demon_io.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os.path as op
 
 import numpy as np
diff -pruN 3.24.0-1/ase/calculators/demonnano.py 3.26.0-1/ase/calculators/demonnano.py
--- 3.24.0-1/ase/calculators/demonnano.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/demonnano.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """This module defines an ASE interface to deMon-nano.
 
diff -pruN 3.24.0-1/ase/calculators/dftb.py 3.26.0-1/ase/calculators/dftb.py
--- 3.24.0-1/ase/calculators/dftb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/dftb.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ This module defines a FileIOCalculator for DFTB+
 
 http://www.dftbplus.org/
diff -pruN 3.24.0-1/ase/calculators/dftd3.py 3.26.0-1/ase/calculators/dftd3.py
--- 3.24.0-1/ase/calculators/dftd3.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/dftd3.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import subprocess
 from pathlib import Path
@@ -313,20 +315,14 @@ class PureDFTD3(FileIOCalculator):
         else:
             damppars = None
 
-        pbc = any(atoms.pbc)
-        if pbc and not all(atoms.pbc):
-            warn('WARNING! dftd3 can only calculate the dispersion energy '
-                 'of non-periodic or 3D-periodic systems. We will treat '
-                 'this system as 3D-periodic!')
-
         if self.comm.rank == 0:
             self._actually_write_input(
                 directory=Path(self.directory), atoms=atoms,
                 properties=properties, prefix=self.label,
-                damppars=damppars, pbc=pbc)
+                damppars=damppars, pbc=any(atoms.pbc))
 
     def _actually_write_input(self, directory, prefix, atoms, properties,
-                              damppars, pbc):
+                              damppars, pbc: bool):
         if pbc:
             fname = directory / f'{prefix}.POSCAR'
             # We sort the atoms so that the atomtypes list becomes as
diff -pruN 3.24.0-1/ase/calculators/dmol.py 3.26.0-1/ase/calculators/dmol.py
--- 3.24.0-1/ase/calculators/dmol.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/dmol.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines an ASE interface to DMol3.
 
 Contacts
diff -pruN 3.24.0-1/ase/calculators/eam.py 3.26.0-1/ase/calculators/eam.py
--- 3.24.0-1/ase/calculators/eam.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/eam.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """Calculator for the Embedded Atom Method Potential"""
 
@@ -17,6 +19,7 @@ from ase.calculators.calculator import C
 from ase.data import chemical_symbols
 from ase.neighborlist import NeighborList
 from ase.units import Bohr, Hartree
+from ase.stress import full_3x3_to_voigt_6_stress
 
 
 class EAM(Calculator):
@@ -95,7 +98,7 @@ by element types.
 Running the Calculator
 ======================
 
-EAM calculates the cohesive atom energy and forces. Internally the
+EAM calculates the cohesive atom energy, forces and stress. Internally the
 potential functions are defined by splines which may be directly
 supplied or created by reading the spline points from a data file from
 which a spline function is created.  The LAMMPS compatible ``.alloy``, ``.fs``
@@ -114,6 +117,7 @@ For example::
     slab.calc = mishin
     slab.get_potential_energy()
     slab.get_forces()
+    slab.get_stress()
 
 The breakdown of energy contribution from the indvidual components are
 stored in the calculator instance ``.results['energy_components']``
@@ -232,7 +236,7 @@ Notes/Issues
 End EAM Interface Documentation
     """
 
-    implemented_properties = ['energy', 'forces']
+    implemented_properties = ['energy', 'free_energy', 'forces', 'stress']
 
     default_parameters = dict(
         skin=1.0,
@@ -680,7 +684,7 @@ End EAM Interface Documentation
             self.update(self.atoms)
             self.calculate_energy(self.atoms)
 
-            if 'forces' in properties:
+            if 'forces' in properties or 'stress' in properties:
                 self.calculate_forces(self.atoms)
 
         # check we have all the properties requested
@@ -689,7 +693,7 @@ End EAM Interface Documentation
                 if property == 'energy':
                     self.calculate_energy(self.atoms)
 
-                if property == 'forces':
+                if property in ['forces', 'stress']:
                     self.calculate_forces(self.atoms)
 
         # we need to remember the previous state of parameters
@@ -783,12 +787,14 @@ End EAM Interface Documentation
 
         self.results['energy_components'] = components
         self.results['energy'] = energy
+        self.results['free_energy'] = energy
 
     def calculate_forces(self, atoms):
         # calculate the forces based on derivatives of the three EAM functions
 
         self.update(atoms)
         self.results['forces'] = np.zeros((len(atoms), 3))
+        stresses = np.zeros((len(atoms), 3, 3))
 
         for i in range(len(atoms)):  # this is the atom to be embedded
             neighbors, offsets = self.neighbors.get_neighbors(i)
@@ -827,9 +833,12 @@ End EAM Interface Documentation
                               self.d_electron_density[self.index[i]](rnuse)))
 
                 self.results['forces'][i] += np.dot(scale, urvec[nearest][use])
+                stresses[i] += np.dot(
+                                (scale[:,np.newaxis] * urvec[nearest][use]).T,
+                                rvec[nearest][use])
 
                 if self.form == 'adp':
-                    adp_forces = self.angular_forces(
+                    adp_forces, adp_stresses = self.angular_forces(
                         self.mu[i],
                         self.mu[neighbors[nearest][use]],
                         self.lam[i],
@@ -840,6 +849,11 @@ End EAM Interface Documentation
                         j_index)
 
                     self.results['forces'][i] += adp_forces
+                    stresses[i] += adp_stresses
+        
+        if self.atoms.cell.rank == 3:
+            stress = 0.5 * np.sum(stresses, axis=0) / self.atoms.get_volume()
+            self.results['stress'] = full_3x3_to_voigt_6_stress(stress)
 
     def angular_forces(self, mu_i, mu, lam_i, lam, r, rvec, form1, form2):
         # calculate the extra components for the adp forces
@@ -872,7 +886,7 @@ End EAM Interface Documentation
             # on the NIST website with the AlH potential
             psi[:, gamma] = term1 + term2 + term3 + term4 - term5
 
-        return np.sum(psi, axis=0)
+        return np.sum(psi, axis=0), np.dot(psi.T, rvec)
 
     def adp_dipole(self, r, rvec, d):
         # calculate the dipole contribution
diff -pruN 3.24.0-1/ase/calculators/elk.py 3.26.0-1/ase/calculators/elk.py
--- 3.24.0-1/ase/calculators/elk.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/elk.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,44 +1,102 @@
-from pathlib import Path
+"""
+`Elk <https://elk.sourceforge.io>`_ is an all-electron full-potential linearised
+augmented-plane wave (LAPW) code.
 
-from ase.calculators.abc import GetOutputsMixin
-from ase.calculators.calculator import FileIOCalculator
-from ase.io import write
-from ase.io.elk import ElkReader
-
-
-class ELK(FileIOCalculator, GetOutputsMixin):
-    _legacy_default_command = 'elk > elk.out'
-    implemented_properties = ['energy', 'forces']
-    ignored_changes = {'pbc'}
-    discard_results_on_any_change = True
-
-    fileio_rules = FileIOCalculator.ruleset(
-        stdout_name='elk.out')
-
-    def __init__(self, **kwargs):
-        """Construct ELK calculator.
-
-        The keyword arguments (kwargs) can be one of the ASE standard
-        keywords: 'xc', 'kpts' and 'smearing' or any of ELK'
-        native keywords.
-        """
+.. versionchanged:: 3.26.0
+   :class:`ELK` is now a subclass of :class:`GenericFileIOCalculator`.
+
+.. |config| replace:: ``config.ini``
+.. _config: calculators.html#calculator-configuration
+
+:class:`ELK` can be configured with |config|_.
+
+.. code-block:: ini
+
+    [elk]
+    command = /path/to/elk
+    sppath = /path/to/species
+
+If you need to override it for programmatic control of the ``elk`` command,
+use :class:`ElkProfile`.
+
+.. code-block:: python
+
+    from ase.calculators.elk import ELK, ElkProfile
+
+    profile = ElkProfile(command='/path/to/elk')
+    calc = ELK(profile=profile)
 
-        super().__init__(**kwargs)
+"""
 
-    def write_input(self, atoms, properties=None, system_changes=None):
-        FileIOCalculator.write_input(self, atoms, properties, system_changes)
+import os
+import re
+import warnings
+from pathlib import Path
+from typing import Optional
 
-        parameters = dict(self.parameters)
+from ase.calculators.genericfileio import (
+    BaseProfile,
+    CalculatorTemplate,
+    GenericFileIOCalculator,
+    read_stdout,
+)
+from ase.io.elk import ElkReader, write_elk_in
+
+COMPATIBILITY_MSG = (
+    '`ELK` has been restructured. '
+    'Please use `ELK(profile=ElkProfile(command))` instead.'
+)
+
+
+class ElkProfile(BaseProfile):
+    """Profile for :class:`ELK`."""
+
+    configvars = {'sppath'}
+
+    def __init__(self, command, sppath: Optional[str] = None, **kwargs) -> None:
+        super().__init__(command, **kwargs)
+        self.sppath = sppath
+
+    def get_calculator_command(self, inputfile):
+        return []
+
+    def version(self):
+        output = read_stdout(self._split_command)
+        match = re.search(r'Elk code version (\S+)', output, re.M)
+        return match.group(1)
+
+
+class ElkTemplate(CalculatorTemplate):
+    """Template for :class:`ELK`."""
+
+    def __init__(self):
+        super().__init__('elk', ['energy', 'forces'])
+        self.inputname = 'elk.in'
+        self.outputname = 'elk.out'
+
+    def write_input(
+        self,
+        profile: ElkProfile,
+        directory,
+        atoms,
+        parameters,
+        properties,
+    ):
+        directory = Path(directory)
+        parameters = dict(parameters)
         if 'forces' in properties:
             parameters['tforce'] = True
+        if 'sppath' not in parameters and profile.sppath:
+            parameters['sppath'] = profile.sppath
+        write_elk_in(directory / self.inputname, atoms, parameters=parameters)
 
-        directory = Path(self.directory)
-        write(directory / 'elk.in', atoms, parameters=parameters,
-              format='elk-in')
+    def execute(self, directory, profile: ElkProfile) -> None:
+        profile.run(directory, self.inputname, self.outputname)
 
-    def read_results(self):
+    def read_results(self, directory):
         from ase.outputs import Properties
-        reader = ElkReader(self.directory)
+
+        reader = ElkReader(directory)
         dct = dict(reader.read_everything())
 
         converged = dct.pop('converged')
@@ -47,7 +105,50 @@ class ELK(FileIOCalculator, GetOutputsMi
 
         # (Filter results thorugh Properties for error detection)
         props = Properties(dct)
-        self.results = dict(props)
+        return dict(props)
+
+    def load_profile(self, cfg, **kwargs):
+        return ElkProfile.from_config(cfg, self.name, **kwargs)
+
+
+class ELK(GenericFileIOCalculator):
+    """Elk calculator."""
+
+    def __init__(
+        self,
+        *,
+        profile=None,
+        command=GenericFileIOCalculator._deprecated,
+        label=GenericFileIOCalculator._deprecated,
+        directory='.',
+        **kwargs,
+    ) -> None:
+        """
+
+        Parameters
+        ----------
+        **kwargs : dict, optional
+            ASE standard keywords like ``xc``, ``kpts`` and ``smearing`` or any
+            Elk-native keywords.
+
+        Examples
+        --------
+        >>> calc = ELK(tasks=0, ngridk=(3, 3, 3))
+
+        """
+        if command is not self._deprecated:
+            raise RuntimeError(COMPATIBILITY_MSG)
 
-    def _outputmixin_get_results(self):
-        return self.results
+        if label is not self._deprecated:
+            msg = 'Ignoring label, please use directory instead'
+            warnings.warn(msg, FutureWarning)
+
+        if 'ASE_ELK_COMMAND' in os.environ and profile is None:
+            warnings.warn(COMPATIBILITY_MSG, FutureWarning)
+
+        super().__init__(
+            template=ElkTemplate(),
+            profile=profile,
+            directory=directory,
+            parameters=kwargs,
+        )
diff -pruN 3.24.0-1/ase/calculators/emt.py 3.26.0-1/ase/calculators/emt.py
--- 3.24.0-1/ase/calculators/emt.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/emt.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Effective medium theory potential."""
 from collections import defaultdict
 from math import log, sqrt
diff -pruN 3.24.0-1/ase/calculators/espresso.py 3.26.0-1/ase/calculators/espresso.py
--- 3.24.0-1/ase/calculators/espresso.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/espresso.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Quantum ESPRESSO Calculator
 
 Run pw.x jobs.
diff -pruN 3.24.0-1/ase/calculators/excitation_list.py 3.26.0-1/ase/calculators/excitation_list.py
--- 3.24.0-1/ase/calculators/excitation_list.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/excitation_list.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.units import Bohr, Hartree
diff -pruN 3.24.0-1/ase/calculators/exciting/__init__.py 3.26.0-1/ase/calculators/exciting/__init__.py
--- 3.24.0-1/ase/calculators/exciting/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/exciting/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from .exciting import ExcitingGroundStateCalculator
 
 __all__ = ["ExcitingGroundStateCalculator"]
diff -pruN 3.24.0-1/ase/calculators/exciting/exciting.py 3.26.0-1/ase/calculators/exciting/exciting.py
--- 3.24.0-1/ase/calculators/exciting/exciting.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/exciting/exciting.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ASE Calculator for the ground state exciting DFT code.
 
 Exciting calculator class in this file allow for writing exciting input
diff -pruN 3.24.0-1/ase/calculators/exciting/runner.py 3.26.0-1/ase/calculators/exciting/runner.py
--- 3.24.0-1/ase/calculators/exciting/runner.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/exciting/runner.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Binary runner and results class."""
 import os
 import subprocess
diff -pruN 3.24.0-1/ase/calculators/fd.py 3.26.0-1/ase/calculators/fd.py
--- 3.24.0-1/ase/calculators/fd.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/fd.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,7 @@
+"""Module for `FiniteDifferenceCalculator`."""
+
 from collections.abc import Iterable
+from functools import partial
 from typing import Optional
 
 import numpy as np
@@ -20,8 +23,10 @@ class FiniteDifferenceCalculator(BaseCal
     def __init__(
         self,
         calc: BaseCalculator,
-        eps_disp: float = 1e-6,
-        eps_strain: float = 1e-6,
+        eps_disp: Optional[float] = 1e-6,
+        eps_strain: Optional[float] = 1e-6,
+        *,
+        force_consistent: bool = True,
     ) -> None:
         """
 
@@ -29,26 +34,47 @@ class FiniteDifferenceCalculator(BaseCal
         ----------
         calc : :class:`~ase.calculators.calculator.BaseCalculator`
             ASE Calculator object to be wrapped.
-        eps_disp : float, default 1e-6
+        eps_disp : Optional[float], default 1e-6
             Displacement used for computing forces.
-        eps_strain : float, default 1e-6
+            If :py:obj:`None`, analytical forces are computed.
+        eps_strain : Optional[float], default 1e-6
             Strain used for computing stress.
+            If :py:obj:`None`, analytical stress is computed.
+        force_consistent : bool, default :py:obj:`True`
+            If :py:obj:`True`, the energies consistent with the forces are used
+            for finite-difference calculations.
 
         """
         super().__init__()
         self.calc = calc
         self.eps_disp = eps_disp
         self.eps_strain = eps_strain
+        self.force_consistent = force_consistent
 
     def calculate(self, atoms: Atoms, properties, system_changes) -> None:
         atoms = atoms.copy()  # copy to not mess up original `atoms`
         atoms.calc = self.calc
-        self.results = {
-            'energy': atoms.get_potential_energy(),
-            'forces': calculate_numerical_forces(atoms, eps=self.eps_disp),
-            'stress': calculate_numerical_stress(atoms, eps=self.eps_strain),
-        }
-        self.results['free_energy'] = self.results['energy']
+        self.results = {}
+        self.results['energy'] = self.calc.get_potential_energy(atoms)
+        for key in ['free_energy']:
+            if key in self.calc.results:
+                self.results[key] = self.calc.results[key]
+        if self.eps_disp is None:
+            self.results['forces'] = self.calc.get_forces(atoms)
+        else:
+            self.results['forces'] = calculate_numerical_forces(
+                atoms,
+                eps=self.eps_disp,
+                force_consistent=self.force_consistent,
+            )
+        if self.eps_strain is None:
+            self.results['stress'] = self.calc.get_stress(atoms)
+        else:
+            self.results['stress'] = calculate_numerical_stress(
+                atoms,
+                eps=self.eps_strain,
+                force_consistent=self.force_consistent,
+            )
 
 
 def _numeric_force(
@@ -56,6 +82,8 @@ def _numeric_force(
     iatom: int,
     icart: int,
     eps: float = 1e-6,
+    *,
+    force_consistent: bool = False,
 ) -> float:
     """Calculate numerical force on a specific atom along a specific direction.
 
@@ -69,16 +97,19 @@ def _numeric_force(
         Index of Cartesian component.
     eps : float, default 1e-6
         Displacement.
+    force_consistent : bool, default :py:obj:`False`
+        If :py:obj:`True`, the energies consistent with the forces are used for
+        finite-difference calculations.
 
     """
     p0 = atoms.get_positions()
     p = p0.copy()
     p[iatom, icart] = p0[iatom, icart] + eps
     atoms.set_positions(p, apply_constraint=False)
-    eplus = atoms.get_potential_energy()
+    eplus = atoms.get_potential_energy(force_consistent=force_consistent)
     p[iatom, icart] = p0[iatom, icart] - eps
     atoms.set_positions(p, apply_constraint=False)
-    eminus = atoms.get_potential_energy()
+    eminus = atoms.get_potential_energy(force_consistent=force_consistent)
     atoms.set_positions(p0, apply_constraint=False)
     return (eminus - eplus) / (2 * eps)
 
@@ -88,6 +119,8 @@ def calculate_numerical_forces(
     eps: float = 1e-6,
     iatoms: Optional[Iterable[int]] = None,
     icarts: Optional[Iterable[int]] = None,
+    *,
+    force_consistent: bool = False,
 ) -> np.ndarray:
     """Calculate forces numerically based on the finite-difference method.
 
@@ -103,6 +136,9 @@ def calculate_numerical_forces(
     icarts : Optional[Iterable[int]]
         Indices of Cartesian coordinates for which forces are computed.
         By default, all three coordinates are considered.
+    force_consistent : bool, default :py:obj:`False`
+        If :py:obj:`True`, the energies consistent with the forces are used for
+        finite-difference calculations.
 
     Returns
     -------
@@ -114,18 +150,17 @@ def calculate_numerical_forces(
         iatoms = range(len(atoms))
     if icarts is None:
         icarts = [0, 1, 2]
-    return np.array(
-        [
-            [_numeric_force(atoms, iatom, icart, eps) for icart in icarts]
-            for iatom in iatoms
-        ]
-    )
+    f = partial(_numeric_force, eps=eps, force_consistent=force_consistent)
+    forces = [[f(atoms, iatom, icart) for icart in icarts] for iatom in iatoms]
+    return np.array(forces)
 
 
 def calculate_numerical_stress(
     atoms: Atoms,
     eps: float = 1e-6,
     voigt: bool = True,
+    *,
+    force_consistent: bool = True,
 ) -> np.ndarray:
     """Calculate stress numerically based on the finite-difference method.
 
@@ -135,8 +170,11 @@ def calculate_numerical_stress(
         ASE :class:`~ase.Atoms` object.
     eps : float, default 1e-6
         Strain in the Voigt notation.
-    voigt : bool, default True
-        If True, the stress is returned in the Voigt notation.
+    voigt : bool, default :py:obj:`True`
+        If :py:obj:`True`, the stress is returned in the Voigt notation.
+    force_consistent : bool, default :py:obj:`True`
+        If :py:obj:`True`, the energies consistent with the forces are used for
+        finite-difference calculations.
 
     Returns
     -------
@@ -152,11 +190,11 @@ def calculate_numerical_stress(
         x = np.eye(3)
         x[i, i] = 1.0 + eps
         atoms.set_cell(cell @ x, scale_atoms=True)
-        eplus = atoms.get_potential_energy(force_consistent=True)
+        eplus = atoms.get_potential_energy(force_consistent=force_consistent)
 
         x[i, i] = 1.0 - eps
         atoms.set_cell(cell @ x, scale_atoms=True)
-        eminus = atoms.get_potential_energy(force_consistent=True)
+        eminus = atoms.get_potential_energy(force_consistent=force_consistent)
 
         stress[i, i] = (eplus - eminus) / (2 * eps * volume)
         x[i, i] = 1.0
@@ -164,11 +202,11 @@ def calculate_numerical_stress(
         j = i - 2
         x[i, j] = x[j, i] = +0.5 * eps
         atoms.set_cell(cell @ x, scale_atoms=True)
-        eplus = atoms.get_potential_energy(force_consistent=True)
+        eplus = atoms.get_potential_energy(force_consistent=force_consistent)
 
         x[i, j] = x[j, i] = -0.5 * eps
         atoms.set_cell(cell @ x, scale_atoms=True)
-        eminus = atoms.get_potential_energy(force_consistent=True)
+        eminus = atoms.get_potential_energy(force_consistent=force_consistent)
 
         stress[i, j] = stress[j, i] = (eplus - eminus) / (2 * eps * volume)
 
diff -pruN 3.24.0-1/ase/calculators/ff.py 3.26.0-1/ase/calculators/ff.py
--- 3.24.0-1/ase/calculators/ff.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/ff.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.calculators.calculator import Calculator
diff -pruN 3.24.0-1/ase/calculators/fleur.py 3.26.0-1/ase/calculators/fleur.py
--- 3.24.0-1/ase/calculators/fleur.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/fleur.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 def FLEUR(*args, **kwargs):
     raise RuntimeError(
         'Please refer to the python package ase-fleur for '
diff -pruN 3.24.0-1/ase/calculators/gamess_us.py 3.26.0-1/ase/calculators/gamess_us.py
--- 3.24.0-1/ase/calculators/gamess_us.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/gamess_us.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import warnings
 
 from ase.calculators.calculator import FileIOCalculator
diff -pruN 3.24.0-1/ase/calculators/gaussian.py 3.26.0-1/ase/calculators/gaussian.py
--- 3.24.0-1/ase/calculators/gaussian.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/gaussian.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import copy
 import os
 from collections.abc import Iterable
diff -pruN 3.24.0-1/ase/calculators/genericfileio.py 3.26.0-1/ase/calculators/genericfileio.py
--- 3.24.0-1/ase/calculators/genericfileio.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/genericfileio.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import shlex
 from abc import ABC, abstractmethod
 from contextlib import ExitStack
@@ -14,7 +16,7 @@ from ase.calculators.calculator import (
 from ase.config import cfg as _cfg
 
 link_calculator_docs = (
-    "https://wiki.fysik.dtu.dk/ase/ase/calculators/"
+    "https://ase-lib.org/ase/calculators/"
     "calculators.html#calculator-configuration"
 )
 
diff -pruN 3.24.0-1/ase/calculators/gromacs.py 3.26.0-1/ase/calculators/gromacs.py
--- 3.24.0-1/ase/calculators/gromacs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/gromacs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines an ASE interface to GROMACS.
 
 http://www.gromacs.org/
diff -pruN 3.24.0-1/ase/calculators/gulp.py 3.26.0-1/ase/calculators/gulp.py
--- 3.24.0-1/ase/calculators/gulp.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/gulp.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines an ASE interface to GULP.
 
 Written by:
diff -pruN 3.24.0-1/ase/calculators/h2morse.py 3.26.0-1/ase/calculators/h2morse.py
--- 3.24.0-1/ase/calculators/h2morse.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/h2morse.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from itertools import count
 
 import numpy as np
diff -pruN 3.24.0-1/ase/calculators/harmonic.py 3.26.0-1/ase/calculators/harmonic.py
--- 3.24.0-1/ase/calculators/harmonic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/harmonic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 from numpy.linalg import eigh, norm, pinv
 from scipy.linalg import lstsq  # performs better than numpy.linalg.lstsq
diff -pruN 3.24.0-1/ase/calculators/idealgas.py 3.26.0-1/ase/calculators/idealgas.py
--- 3.24.0-1/ase/calculators/idealgas.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/idealgas.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Ideal gas calculator - the potential energy is always zero."""
 
 import numpy as np
diff -pruN 3.24.0-1/ase/calculators/kim/__init__.py 3.26.0-1/ase/calculators/kim/__init__.py
--- 3.24.0-1/ase/calculators/kim/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/kim/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from .kim import KIM, get_model_supported_species
 from .kimpy_wrappers import kimpy
 
diff -pruN 3.24.0-1/ase/calculators/kim/calculators.py 3.26.0-1/ase/calculators/kim/calculators.py
--- 3.24.0-1/ase/calculators/kim/calculators.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/kim/calculators.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import re
 
diff -pruN 3.24.0-1/ase/calculators/kim/exceptions.py 3.26.0-1/ase/calculators/kim/exceptions.py
--- 3.24.0-1/ase/calculators/kim/exceptions.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/kim/exceptions.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Exceptions for the general error types that can occur either while
 setting up the calculator, which requires constructing KIM API C++
diff -pruN 3.24.0-1/ase/calculators/kim/kim.py 3.26.0-1/ase/calculators/kim/kim.py
--- 3.24.0-1/ase/calculators/kim/kim.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/kim/kim.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Knowledgebase of Interatomic Models (KIM) Calculator for ASE written by:
 
@@ -86,7 +88,7 @@ def KIM(model_name, simulator=None, opti
           model to run with multiple concurrent threads. (Default: False)
 
         See the ASE LAMMPS calculators doc page
-        (https://wiki.fysik.dtu.dk/ase/ase/calculators/lammps.html) for
+        (https://ase-lib.org/ase/calculators/lammps.html) for
         available options for the lammpslib and lammpsrun calculators.
 
     debug : bool, optional
diff -pruN 3.24.0-1/ase/calculators/kim/kimmodel.py 3.26.0-1/ase/calculators/kim/kimmodel.py
--- 3.24.0-1/ase/calculators/kim/kimmodel.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/kim/kimmodel.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 ASE Calculator for interatomic models compatible with the Knowledgebase
 of Interatomic Models (KIM) application programming interface (API).
diff -pruN 3.24.0-1/ase/calculators/kim/kimpy_wrappers.py 3.26.0-1/ase/calculators/kim/kimpy_wrappers.py
--- 3.24.0-1/ase/calculators/kim/kimpy_wrappers.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/kim/kimpy_wrappers.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Wrappers that provide a minimal interface to kimpy methods and objects
 
diff -pruN 3.24.0-1/ase/calculators/kim/neighborlist.py 3.26.0-1/ase/calculators/kim/neighborlist.py
--- 3.24.0-1/ase/calculators/kim/neighborlist.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/kim/neighborlist.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from collections import defaultdict
 
 import numpy as np
diff -pruN 3.24.0-1/ase/calculators/lammps/__init__.py 3.26.0-1/ase/calculators/lammps/__init__.py
--- 3.24.0-1/ase/calculators/lammps/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/lammps/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Collection of helper function for LAMMPS* calculator
 """
 from .coordinatetransform import Prism
diff -pruN 3.24.0-1/ase/calculators/lammps/coordinatetransform.py 3.26.0-1/ase/calculators/lammps/coordinatetransform.py
--- 3.24.0-1/ase/calculators/lammps/coordinatetransform.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/lammps/coordinatetransform.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,6 +1,8 @@
+# fmt: off
+
 """Prism"""
 import warnings
-from typing import Sequence
+from typing import Sequence, Union
 
 import numpy as np
 
@@ -38,7 +40,10 @@ def calc_rotated_cell(cell: np.ndarray)
     return np.array(((ax, 0.0, 0.0), (bx, by, 0.0), (cx, cy, cz)))
 
 
-def calc_reduced_cell(cell: np.ndarray, pbc: Sequence[bool]) -> np.ndarray:
+def calc_reduced_cell(
+    cell: np.ndarray,
+    pbc: Union[np.ndarray, Sequence[bool]],
+) -> np.ndarray:
     """Calculate LAMMPS cell with short lattice basis vectors
 
     The lengths of the second and the third lattice basis vectors, b and c, are
@@ -137,7 +142,7 @@ class Prism:
     def __init__(
         self,
         cell: np.ndarray,
-        pbc: bool = True,
+        pbc: Union[bool, np.ndarray] = True,
         reduce_cell: bool = False,
         tolerance: float = 1.0e-8,
     ):
diff -pruN 3.24.0-1/ase/calculators/lammps/inputwriter.py 3.26.0-1/ase/calculators/lammps/inputwriter.py
--- 3.24.0-1/ase/calculators/lammps/inputwriter.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/lammps/inputwriter.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Stream input commands to lammps to perform desired simulations
 """
diff -pruN 3.24.0-1/ase/calculators/lammps/unitconvert.py 3.26.0-1/ase/calculators/lammps/unitconvert.py
--- 3.24.0-1/ase/calculators/lammps/unitconvert.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/lammps/unitconvert.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """LAMMPS has the options to use several internal units (which can be different
 from the ones used in ase).  Mapping is therefore necessary.
 
diff -pruN 3.24.0-1/ase/calculators/lammps/unitconvert_constants.py 3.26.0-1/ase/calculators/lammps/unitconvert_constants.py
--- 3.24.0-1/ase/calculators/lammps/unitconvert_constants.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/lammps/unitconvert_constants.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # The following definitions are all given in SI and are excerpted from the
 # kim_units.cpp file created by Prof. Ellad B. Tadmor (UMinn) distributed with
 # LAMMPS. Note that these values do not correspond to any official CODATA set
diff -pruN 3.24.0-1/ase/calculators/lammpslib.py 3.26.0-1/ase/calculators/lammpslib.py
--- 3.24.0-1/ase/calculators/lammpslib.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/lammpslib.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ASE LAMMPS Calculator Library Version"""
 
 
@@ -495,10 +497,13 @@ xz and yz are the tilt of the lattice ve
         )
         self.results['free_energy'] = self.results['energy']
 
-        ids = self.lmp.numpy.extract_atom("id")
-        # if ids doesn't match atoms then data is MPI distributed, which
-        # we can't handle
-        assert len(ids) == len(atoms)
+        # check for MPI active as per https://matsci.org/t/lammps-ids-in-python/60509/5
+        world_size = self.lmp.extract_setting('world_size')
+        if world_size != 1:
+            raise RuntimeError('Unsupported MPI active as indicated by '
+                               f'world_size == {world_size} != 1')
+        # select just n_local which we assume is equal to len(atoms)
+        ids = self.lmp.numpy.extract_atom("id")[0:len(atoms)]
         self.results["energies"] = convert(
             self.lmp.numpy.extract_compute('pe_peratom',
                                            self.LMP_STYLE_ATOM,
diff -pruN 3.24.0-1/ase/calculators/lammpsrun.py 3.26.0-1/ase/calculators/lammpsrun.py
--- 3.24.0-1/ase/calculators/lammpsrun.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/lammpsrun.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ASE calculator for the LAMMPS classical MD code"""
 # lammps.py (2011/03/29)
 #
diff -pruN 3.24.0-1/ase/calculators/lj.py 3.26.0-1/ase/calculators/lj.py
--- 3.24.0-1/ase/calculators/lj.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/lj.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.calculators.calculator import Calculator, all_changes
diff -pruN 3.24.0-1/ase/calculators/loggingcalc.py 3.26.0-1/ase/calculators/loggingcalc.py
--- 3.24.0-1/ase/calculators/loggingcalc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/loggingcalc.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Provides LoggingCalculator class to wrap a Calculator and record
 number of enery and force calls
diff -pruN 3.24.0-1/ase/calculators/mixing.py 3.26.0-1/ase/calculators/mixing.py
--- 3.24.0-1/ase/calculators/mixing.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/mixing.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.calculators.calculator import (
     BaseCalculator,
     CalculatorSetupError,
diff -pruN 3.24.0-1/ase/calculators/mopac.py 3.26.0-1/ase/calculators/mopac.py
--- 3.24.0-1/ase/calculators/mopac.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/mopac.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines an ASE interface to MOPAC.
 
 Set $ASE_MOPAC_COMMAND to something like::
diff -pruN 3.24.0-1/ase/calculators/morse.py 3.26.0-1/ase/calculators/morse.py
--- 3.24.0-1/ase/calculators/morse.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/morse.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,7 +1,9 @@
+# fmt: off
+
 import numpy as np
 
 from ase.calculators.calculator import Calculator, all_changes
-from ase.neighborlist import neighbor_list
+from ase.neighborlist import neighbor_list as ase_neighbor_list
 from ase.stress import full_3x3_to_voigt_6_stress
 
 
@@ -58,7 +60,7 @@ class MorsePotential(Calculator):
                           'rcut2': 2.7}
     nolabel = True
 
-    def __init__(self, **kwargs):
+    def __init__(self, neighbor_list=ase_neighbor_list, **kwargs):
         r"""
 
         The pairwise energy between atoms *i* and *j* is given by
@@ -88,12 +90,16 @@ class MorsePotential(Calculator):
             Distance starting a smooth cutoff normalized by ``r0``.
         rcut2 : float, default 2.7
             Distance ending a smooth cutoff normalized by ``r0``.
+        neighbor_list : callable, optional
+            neighbor_list function compatible with
+            ase.neighborlist.neighbor_list
 
         Notes
         -----
         The default values are chosen to be similar as Lennard-Jones.
 
         """
+        self.neighbor_list = neighbor_list
         Calculator.__init__(self, **kwargs)
 
     def calculate(self, atoms=None, properties=['energy'],
@@ -109,7 +115,7 @@ class MorsePotential(Calculator):
 
         forces = np.zeros((number_of_atoms, 3))
 
-        i, _j, d, D = neighbor_list('ijdD', atoms, rcut2)
+        i, _j, d, D = self.neighbor_list('ijdD', atoms, rcut2)
         dhat = (D / d[:, None]).T
 
         expf = np.exp(rho0 * (1.0 - d / r0))
diff -pruN 3.24.0-1/ase/calculators/names.py 3.26.0-1/ase/calculators/names.py
--- 3.24.0-1/ase/calculators/names.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/names.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import importlib
 from collections.abc import Mapping
 
@@ -8,11 +10,11 @@ names = ['abinit', 'ace', 'aims', 'amber
          'gaussian', 'gpaw', 'gromacs', 'gulp', 'hotbit', 'kim',
          'lammpslib', 'lammpsrun', 'lj', 'mopac', 'morse', 'nwchem',
          'octopus', 'onetep', 'openmx', 'orca',
-         'plumed', 'psi4', 'qchem', 'siesta',
+         'plumed', 'psi4', 'qchem', 'siesta', 'tersoff',
          'tip3p', 'tip4p', 'turbomole', 'vasp']
 
 
-builtin = {'eam', 'emt', 'ff', 'lj', 'morse', 'tip3p', 'tip4p'}
+builtin = {'eam', 'emt', 'ff', 'lj', 'morse', 'tersoff', 'tip3p', 'tip4p'}
 
 
 class Templates(Mapping):
diff -pruN 3.24.0-1/ase/calculators/nwchem.py 3.26.0-1/ase/calculators/nwchem.py
--- 3.24.0-1/ase/calculators/nwchem.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/nwchem.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines an ASE interface to NWchem
 
 https://nwchemgit.github.io
diff -pruN 3.24.0-1/ase/calculators/octopus.py 3.26.0-1/ase/calculators/octopus.py
--- 3.24.0-1/ase/calculators/octopus.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/octopus.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ASE-interface to Octopus.
 
 Ask Hjorth Larsen <asklarsen@gmail.com>
diff -pruN 3.24.0-1/ase/calculators/onetep.py 3.26.0-1/ase/calculators/onetep.py
--- 3.24.0-1/ase/calculators/onetep.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/onetep.py	2025-08-12 11:26:23.000000000 +0000
@@ -29,8 +29,7 @@ class OnetepProfile(BaseProfile):
         command: str
             The onetep command (not including inputfile).
         **kwargs: dict
-            Additional kwargs are passed to the BaseProfile
-            class.
+            Additional kwargs are passed to the BaseProfile class.
         """
         super().__init__(command, **kwargs)
         self.pseudo_path = pseudo_path
@@ -56,15 +55,22 @@ class OnetepTemplate(CalculatorTemplate)
                 'energy',
                 'free_energy',
                 'forces',
-                'stress'])
+                'stress',
+            ],
+        )
         self.inputname = f'{self._label}.dat'
         self.outputname = f'{self._label}.out'
         self.errorname = f'{self._label}.err'
         self.append = append
 
     def execute(self, directory, profile):
-        profile.run(directory, self.inputname, self.outputname,
-                    self.errorname, append=self.append)
+        profile.run(
+            directory,
+            self.inputname,
+            self.outputname,
+            self.errorname,
+            append=self.append,
+        )
 
     def read_results(self, directory):
         output_path = directory / self.outputname
@@ -80,8 +86,13 @@ class OnetepTemplate(CalculatorTemplate)
         keywords.setdefault('pseudo_path', profile.pseudo_path)
         parameters['keywords'] = keywords
 
-        write(input_path, atoms, format='onetep-in',
-              properties=properties, **parameters)
+        write(
+            input_path,
+            atoms,
+            format='onetep-in',
+            properties=properties,
+            **parameters,
+        )
 
     def load_profile(self, cfg, **kwargs):
         return OnetepProfile.from_config(cfg, self.name, **kwargs)
@@ -90,8 +101,6 @@ class OnetepTemplate(CalculatorTemplate)
 class Onetep(GenericFileIOCalculator):
     """
     Class for the ONETEP calculator, uses ase/io/onetep.py.
-    Need the env variable "ASE_ONETEP_COMMAND" defined to
-    properly work. All other options are passed in kwargs.
 
     Parameters
     ----------
@@ -147,18 +156,13 @@ class Onetep(GenericFileIOCalculator):
            are valid ONETEP keywords.
     """
 
-    def __init__(
-            self,
-            *,
-            profile=None,
-            directory='.',
-            **kwargs):
-
+    def __init__(self, *, profile=None, directory='.', **kwargs):
         self.keywords = kwargs.get('keywords', None)
-        self.template = OnetepTemplate(
-            append=kwargs.pop('append', False)
-        )
+        self.template = OnetepTemplate(append=kwargs.pop('append', False))
 
-        super().__init__(profile=profile, template=self.template,
-                         directory=directory,
-                         parameters=kwargs)
+        super().__init__(
+            profile=profile,
+            template=self.template,
+            directory=directory,
+            parameters=kwargs,
+        )
diff -pruN 3.24.0-1/ase/calculators/openmx/default_settings.py 3.26.0-1/ase/calculators/openmx/default_settings.py
--- 3.24.0-1/ase/calculators/openmx/default_settings.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/openmx/default_settings.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 The ASE Calculator for OpenMX <http://www.openmx-square.org>: Python interface
 to the software package for nano-scale material simulations based on density
diff -pruN 3.24.0-1/ase/calculators/openmx/dos.py 3.26.0-1/ase/calculators/openmx/dos.py
--- 3.24.0-1/ase/calculators/openmx/dos.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/openmx/dos.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 The ASE Calculator for OpenMX <http://www.openmx-square.org>: Python interface
 to the software package for nano-scale material simulations based on density
diff -pruN 3.24.0-1/ase/calculators/openmx/openmx.py 3.26.0-1/ase/calculators/openmx/openmx.py
--- 3.24.0-1/ase/calculators/openmx/openmx.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/openmx/openmx.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
     The ASE Calculator for OpenMX <http://www.openmx-square.org>
     A Python interface to the software package for nano-scale
diff -pruN 3.24.0-1/ase/calculators/openmx/parameters.py 3.26.0-1/ase/calculators/openmx/parameters.py
--- 3.24.0-1/ase/calculators/openmx/parameters.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/openmx/parameters.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 The ASE Calculator for OpenMX <http://www.openmx-square.org>: Python interface
 to the software package for nano-scale material simulations based on density
diff -pruN 3.24.0-1/ase/calculators/openmx/reader.py 3.26.0-1/ase/calculators/openmx/reader.py
--- 3.24.0-1/ase/calculators/openmx/reader.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/openmx/reader.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """
 The ASE Calculator for OpenMX <http://www.openmx-square.org>: Python interface
diff -pruN 3.24.0-1/ase/calculators/openmx/writer.py 3.26.0-1/ase/calculators/openmx/writer.py
--- 3.24.0-1/ase/calculators/openmx/writer.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/openmx/writer.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 The ASE Calculator for OpenMX <http://www.openmx-square.org>: Python interface
 to the software package for nano-scale material simulations based on density
diff -pruN 3.24.0-1/ase/calculators/orca.py 3.26.0-1/ase/calculators/orca.py
--- 3.24.0-1/ase/calculators/orca.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/orca.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 
 import ase.io.orca as io
diff -pruN 3.24.0-1/ase/calculators/plumed.py 3.26.0-1/ase/calculators/plumed.py
--- 3.24.0-1/ase/calculators/plumed.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/plumed.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from os.path import exists
 
 import numpy as np
diff -pruN 3.24.0-1/ase/calculators/psi4.py 3.26.0-1/ase/calculators/psi4.py
--- 3.24.0-1/ase/calculators/psi4.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/psi4.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 authors: Ben Comer (Georgia Tech), Xiangyun (Ray) Lei (Georgia Tech)
 
diff -pruN 3.24.0-1/ase/calculators/qchem.py 3.26.0-1/ase/calculators/qchem.py
--- 3.24.0-1/ase/calculators/qchem.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/qchem.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 import ase.units
diff -pruN 3.24.0-1/ase/calculators/qmmm.py 3.26.0-1/ase/calculators/qmmm.py
--- 3.24.0-1/ase/calculators/qmmm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/qmmm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import Sequence
 
 import numpy as np
diff -pruN 3.24.0-1/ase/calculators/siesta/import_ion_xml.py 3.26.0-1/ase/calculators/siesta/import_ion_xml.py
--- 3.24.0-1/ase/calculators/siesta/import_ion_xml.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/siesta/import_ion_xml.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 from xml.dom import minidom
 
diff -pruN 3.24.0-1/ase/calculators/siesta/parameters.py 3.26.0-1/ase/calculators/siesta/parameters.py
--- 3.24.0-1/ase/calculators/siesta/parameters.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/siesta/parameters.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.calculators.calculator import Parameters
 
 """
diff -pruN 3.24.0-1/ase/calculators/siesta/siesta.py 3.26.0-1/ase/calculators/siesta/siesta.py
--- 3.24.0-1/ase/calculators/siesta/siesta.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/siesta/siesta.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 This module defines the ASE interface to SIESTA.
 
diff -pruN 3.24.0-1/ase/calculators/siesta/siesta_lrtddft.py 3.26.0-1/ase/calculators/siesta/siesta_lrtddft.py
--- 3.24.0-1/ase/calculators/siesta/siesta_lrtddft.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/siesta/siesta_lrtddft.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 import ase.units as un
diff -pruN 3.24.0-1/ase/calculators/singlepoint.py 3.26.0-1/ase/calculators/singlepoint.py
--- 3.24.0-1/ase/calculators/singlepoint.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/singlepoint.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,7 @@
+# fmt: off
+
+from functools import cached_property
+
 import numpy as np
 
 from ase.calculators.calculator import (
@@ -7,7 +11,6 @@ from ase.calculators.calculator import (
     all_properties,
 )
 from ase.outputs import Properties
-from ase.utils import lazyproperty
 
 
 class SinglePointCalculator(Calculator):
@@ -227,7 +230,7 @@ def propertygetter(func):
         if value is None:
             raise PropertyNotPresent(func.__name__)
         return value
-    return lazyproperty(getter)
+    return cached_property(getter)
 
 
 class OutputPropertyWrapper:
diff -pruN 3.24.0-1/ase/calculators/socketio.py 3.26.0-1/ase/calculators/socketio.py
--- 3.24.0-1/ase/calculators/socketio.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/socketio.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,6 +1,8 @@
+# fmt: off
+
 import os
 import socket
-from contextlib import contextmanager
+from contextlib import ExitStack, contextmanager
 from subprocess import PIPE, Popen
 
 import numpy as np
@@ -245,6 +247,13 @@ class FileIOSocketClientLauncher:
                 argv = template.socketio_argv(
                     profile, unixsocket=None, port=port
                 )
+
+            if hasattr(self.calc.template, "outputname"):
+                with ExitStack() as stack:
+                    output_path = self.calc.template.outputname
+                    fd_out = stack.enter_context(open(output_path, "w"))
+                    return Popen(argv, cwd=cwd, env=os.environ, stdout=fd_out)
+
             return Popen(argv, cwd=cwd, env=os.environ)
         else:
             # Old FileIOCalculator:
diff -pruN 3.24.0-1/ase/calculators/subprocesscalculator.py 3.26.0-1/ase/calculators/subprocesscalculator.py
--- 3.24.0-1/ase/calculators/subprocesscalculator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/subprocesscalculator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import pickle
 import sys
diff -pruN 3.24.0-1/ase/calculators/tersoff.py 3.26.0-1/ase/calculators/tersoff.py
--- 3.24.0-1/ase/calculators/tersoff.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/calculators/tersoff.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,576 @@
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Dict, List, Tuple, Type, Union
+
+import numpy as np
+
+from ase.calculators.calculator import Calculator, all_changes
+from ase.neighborlist import NeighborList
+from ase.stress import full_3x3_to_voigt_6_stress
+
+__author__ = 'Stefan Bringuier <stefanbringuier@gmail.com>'
+__description__ = 'LAMMPS-style native Tersoff potential for ASE'
+
+# Maximum/minimum exponents for numerical stability
+# in bond order calculation
+_MAX_EXP_ARG = 69.0776e0
+_MIN_EXP_ARG = -69.0776e0
+
+
+@dataclass
+class TersoffParameters:
+    """Parameters for 3 element Tersoff potential interaction.
+
+    Can be instantiated with either positional or keyword arguments:
+        TersoffParameters(1.0, 2.0, ...) or
+        TersoffParameters(m=1.0, gamma=2.0, ...)
+    """
+
+    m: float
+    gamma: float
+    lambda3: float
+    c: float
+    d: float
+    h: float
+    n: float
+    beta: float
+    lambda2: float
+    B: float
+    R: float
+    D: float
+    lambda1: float
+    A: float
+
+    @classmethod
+    def from_list(cls, params: List[float]) -> 'TersoffParameters':
+        """Create TersoffParameters from a list of 14 parameter values."""
+        if len(params) != 14:
+            raise ValueError(f'Expected 14 parameters, got {len(params)}')
+        return cls(*map(float, params))
+
+
+class Tersoff(Calculator):
+    """ASE Calculator for Tersoff interatomic potential.
+
+    .. versionadded:: 3.25.0
+    """
+
+    implemented_properties = [
+        'free_energy',
+        'energy',
+        'energies',
+        'forces',
+        'stress',
+    ]
+
+    def __init__(
+        self,
+        parameters: Dict[Tuple[str, str, str], TersoffParameters],
+        skin: float = 0.3,
+        **kwargs,
+    ) -> None:
+        """
+        Parameters
+        ----------
+        parameters : dict
+            Mapping element combinations to TersoffParameters objects::
+
+                {
+                    ('A', 'B', 'C'): TersoffParameters(
+                        m, gamma, lambda3, c, d, h, n,
+                        beta, lambda2, B, R, D, lambda1, A),
+                    ...
+                }
+
+            where ('A', 'B', 'C') are the elements involved in the interaction.
+        skin : float, default 0.3
+            The skin distance for neighbor list calculations.
+        **kwargs : dict
+            Additional parameters to be passed to
+            :class:`~ase.calculators.Calculator`.
+
+        """
+        Calculator.__init__(self, **kwargs)
+        self.cutoff_skin = skin
+        self.parameters = parameters
+
+    @classmethod
+    def from_lammps(
+        cls: Type['Tersoff'],
+        potential_file: Union[str, Path],
+        skin: float = 0.3,
+        **kwargs,
+    ) -> 'Tersoff':
+        """Make :class:`Tersoff` from a LAMMPS-style Tersoff potential file.
+
+        Parameters
+        ----------
+        potential_file : str or Path
+            The path to a LAMMPS-style Tersoff potential file.
+        skin : float, default 0.3
+            The skin distance for neighbor list calculations.
+        **kwargs : dict
+            Additional parameters to be passed to the
+            ASE Calculator constructor.
+
+        Returns
+        -------
+        :class:`Tersoff`
+            Initialized Tersoff calculator with parameters from the file.
+
+        """
+        parameters = cls.read_lammps_format(potential_file)
+        return cls(parameters=parameters, skin=skin, **kwargs)
+
+    @staticmethod
+    def read_lammps_format(
+        potential_file: Union[str, Path],
+    ) -> Dict[Tuple[str, str, str], TersoffParameters]:
+        """Read the Tersoff potential parameters from a LAMMPS-style file.
+
+        Parameters
+        ----------
+        potential_file : str or Path
+            Path to the LAMMPS-style Tersoff potential file
+
+        Returns
+        -------
+        dict
+            Dictionary mapping element combinations to TersoffParameters objects
+
+        """
+        block_size = 17
+        with Path(potential_file).open('r', encoding='utf-8') as fd:
+            content = (
+                ''.join(
+                    [line for line in fd if not line.strip().startswith('#')]
+                )
+                .replace('\n', ' ')
+                .split()
+            )
+
+        if len(content) % block_size != 0:
+            raise ValueError(
+                'The potential file does not have the correct LAMMPS format.'
+            )
+
+        parameters: Dict[Tuple[str, str, str], TersoffParameters] = {}
+        for i in range(0, len(content), block_size):
+            block = content[i : i + block_size]
+            e1, e2, e3 = block[0], block[1], block[2]
+            current_elements = (e1, e2, e3)
+            params = map(float, block[3:])
+            parameters[current_elements] = TersoffParameters(*params)
+
+        return parameters
+
+    def set_parameters(
+        self,
+        key: Tuple[str, str, str],
+        params: TersoffParameters = None,
+        **kwargs,
+    ) -> None:
+        """Update parameters for a specific element combination.
+
+        Parameters
+        ----------
+        key: Tuple[str, str, str]
+            The element combination key of the parameters to be updated
+        params: TersoffParameters, optional
+            A TersoffParameters instance to completely replace the parameters
+        **kwargs:
+            Individual parameter values to update, e.g. R=2.9
+
+        """
+        if key not in self.parameters:
+            raise KeyError(f"Key '{key}' not found in parameters.")
+
+        if params is not None:
+            if kwargs:
+                raise ValueError('Cannot provide both params and kwargs.')
+            self.parameters[key] = params
+        else:
+            for name, value in kwargs.items():
+                if not hasattr(self.parameters[key], name):
+                    raise ValueError(f'Invalid parameter name: {name}')
+                setattr(self.parameters[key], name, value)
+
+    def _update_nl(self, atoms) -> None:
+        """Update the neighbor list with the parameter R+D cutoffs.
+
+        Parameters
+        ----------
+        atoms: ase.Atoms
+            The atoms to calculate the neighbor list for.
+
+        Notes
+        -----
+        The cutoffs are determined by the parameters of the Tersoff potential.
+        Each atom's cutoff is based on the R+D values from the parameter set
+        where that atom's element appears first in the key tuple.
+
+        """
+        # Get cutoff for each atom based on its element type
+        cutoffs = []
+
+        for symbol in atoms.symbols:
+            # Find first parameter set, element is the first slot
+            param_key = next(
+                key for key in self.parameters.keys() if key[0] == symbol
+            )
+            params = self.parameters[param_key]
+            cutoffs.append(params.R + params.D)
+
+        self.nl = NeighborList(
+            cutoffs,
+            skin=self.cutoff_skin,
+            self_interaction=False,
+            bothways=True,
+        )
+
+        self.nl.update(atoms)
+
+    def calculate(
+        self,
+        atoms=None,
+        properties=None,
+        system_changes=all_changes,
+    ) -> None:
+        """Calculate energy, forces, and stress.
+
+        Notes
+        -----
+        The force and stress are calculated regardless if they are
+        requested, despite some additional overhead cost,
+        therefore they are always stored in the results dict.
+
+        """
+        Calculator.calculate(self, atoms, properties, system_changes)
+
+        # Rebuild neighbor list when any relevant system changes occur
+        checks = {'positions', 'numbers', 'cell', 'pbc'}
+        if any(change in checks for change in system_changes) or not hasattr(
+            self, 'nl'
+        ):
+            self._update_nl(atoms)
+
+        self.results = {}
+        energies = np.zeros(len(atoms))
+        forces = np.zeros((len(atoms), 3))
+        virial = np.zeros((3, 3))
+
+        # Duplicates atoms.get_distances() functionality, but uses
+        # neighbor list's pre-computed offsets for efficiency in a
+        # tight force-calculation loop rather than recompute MIC
+        for i in range(len(atoms)):
+            self._calc_atom_contribution(i, energies, forces, virial)
+
+        self.results['energies'] = energies
+        self.results['energy'] = self.results['free_energy'] = energies.sum()
+        self.results['forces'] = forces
+        # Virial to stress (i.e., eV/A^3)
+        if self.atoms.cell.rank == 3:
+            stress = virial / self.atoms.get_volume()
+            self.results['stress'] = full_3x3_to_voigt_6_stress(stress)
+
+    def _calc_atom_contribution(
+        self,
+        idx_i: int,
+        energies: np.ndarray,
+        forces: np.ndarray,
+        virial: np.ndarray,
+    ) -> None:
+        """Calculate the contributions of a single atom to the properties.
+
+        This function calculates the energy, force, and stress on atom i
+        by looking at i-j pair interactions and the modification made by
+        the bond order term bij with includes 3-body interaction i-j-k.
+
+        Parameters
+        ----------
+        idx_i: int
+            Index of atom i
+        energies: array_like
+            Site energies to be updated.
+        forces: array_like
+            Forces to be updated.
+        virial: array_like
+            Virial tensor to be updated.
+
+        """
+        indices, offsets = self.nl.get_neighbors(idx_i)
+        vectors = self.atoms.positions[indices]
+        vectors += offsets @ self.atoms.cell
+        vectors -= self.atoms.positions[idx_i]
+        distances = np.sqrt(np.add.reduce(vectors**2, axis=1))
+
+        type_i = self.atoms.symbols[idx_i]
+        for j, (idx_j, abs_rij, rij) in enumerate(
+            zip(indices, distances, vectors)
+        ):
+            type_j = self.atoms.symbols[idx_j]
+            key = (type_i, type_j, type_j)
+            params = self.parameters[key]
+
+            rij_hat = rij / abs_rij
+
+            fc = self._calc_fc(abs_rij, params.R, params.D)
+            if fc == 0.0:
+                continue
+
+            zeta = self._calc_zeta(type_i, j, indices, distances, vectors)
+            bij = self._calc_bij(zeta, params.beta, params.n)
+            bij_d = self._calc_bij_d(zeta, params.beta, params.n)
+
+            repulsive = params.A * np.exp(-params.lambda1 * abs_rij)
+            attractive = -params.B * np.exp(-params.lambda2 * abs_rij)
+
+            # distribute the pair energy evenly to be consistent with LAMMPS
+            energies[idx_i] += 0.25 * fc * (repulsive + bij * attractive)
+            energies[idx_j] += 0.25 * fc * (repulsive + bij * attractive)
+
+            dfc = self._calc_fc_d(abs_rij, params.R, params.D)
+            rep_deriv = -params.lambda1 * repulsive
+            att_deriv = -params.lambda2 * attractive
+
+            tmp = dfc * (repulsive + bij * attractive)
+            tmp += fc * (rep_deriv + bij * att_deriv)
+
+            # derivative with respect to the position of atom j
+            grad = 0.5 * tmp * rij_hat
+
+            forces[idx_i] += grad
+            forces[idx_j] -= grad
+
+            virial += np.outer(grad, rij)
+
+            for k, idx_k in enumerate(indices):
+                if k == j:
+                    continue
+
+                type_k = self.atoms.symbols[idx_k]
+                key = (type_i, type_j, type_k)
+                params = self.parameters[key]
+
+                if distances[k] > params.R + params.D:
+                    continue
+
+                rik = vectors[k]
+
+                dztdri, dztdrj, dztdrk = self._calc_zeta_d(rij, rik, params)
+
+                gradi = 0.5 * fc * bij_d * dztdri * attractive
+                gradj = 0.5 * fc * bij_d * dztdrj * attractive
+                gradk = 0.5 * fc * bij_d * dztdrk * attractive
+
+                forces[idx_i] -= gradi
+                forces[idx_j] -= gradj
+                forces[idx_k] -= gradk
+
+                virial += np.outer(gradj, rij)
+                virial += np.outer(gradk, rik)
+
+    def _calc_bij(self, zeta: float, beta: float, n: float) -> float:
+        """Calculate the bond order ``bij`` between atoms ``i`` and ``j``."""
+        tmp = beta * zeta
+        return (1.0 + tmp**n) ** (-1.0 / (2.0 * n))
+
+    def _calc_bij_d(self, zeta: float, beta: float, n: float) -> float:
+        """Calculate the derivative of ``bij`` with respect to ``zeta``."""
+        tmp = beta * zeta
+        return (
+            -0.5
+            * (1.0 + tmp**n) ** (-1.0 - (1.0 / (2.0 * n)))
+            * (beta * tmp ** (n - 1.0))
+        )
+
+    def _calc_zeta(
+        self,
+        type_i: str,
+        j: int,
+        neighbors: np.ndarray,
+        distances: np.ndarray,
+        vectors: np.ndarray,
+    ) -> float:
+        """Calculate ``zeta_ij``."""
+        idx_j = neighbors[j]
+        type_j = self.atoms.symbols[idx_j]
+        abs_rij = distances[j]
+
+        zeta = 0.0
+
+        for k, idx_k in enumerate(neighbors):
+            if k == j:
+                continue
+
+            type_k = self.atoms.symbols[idx_k]
+            key = (type_i, type_j, type_k)
+            params = self.parameters[key]
+
+            abs_rik = distances[k]
+            if abs_rik > params.R + params.D:
+                continue
+
+            costheta = np.dot(vectors[j], vectors[k]) / (abs_rij * abs_rik)
+            fc_ik = self._calc_fc(abs_rik, params.R, params.D)
+
+            g_theta = self._calc_gijk(costheta, params)
+
+            # Calculate the exponential for the bond order zeta term
+            # This is the term that modifies the bond order based
+            # on the distance between atoms i-j and i-k. Tresholds are
+            # used to prevent overflow/underflow.
+            arg = (params.lambda3 * (abs_rij - abs_rik)) ** params.m
+            if arg > _MAX_EXP_ARG:
+                ex_delr = 1.0e30
+            elif arg < _MIN_EXP_ARG:
+                ex_delr = 0.0
+            else:
+                ex_delr = np.exp(arg)
+
+            zeta += fc_ik * g_theta * ex_delr
+
+        return zeta
+
+    def _calc_gijk(self, costheta: float, params: TersoffParameters) -> float:
+        r"""Calculate the angular function ``g`` for the Tersoff potential.
+
+        .. math::
+            g(\theta) = \gamma \left( 1 + \frac{c^2}{d^2}
+            - \frac{c^2}{d^2 + (h - \cos \theta)^2} \right)
+
+        where :math:`\theta` is the angle between the bond vector
+        and the vector of atom i and its neighbors j-k.
+        """
+        c2 = params.c * params.c
+        d2 = params.d * params.d
+        hcth = params.h - costheta
+        return params.gamma * (1.0 + c2 / d2 - c2 / (d2 + hcth**2))
+
+    def _calc_gijk_d(self, costheta: float, params: TersoffParameters) -> float:
+        """Calculate the derivative of ``g`` with respect to ``costheta``."""
+        c2 = params.c * params.c
+        d2 = params.d * params.d
+        hcth = params.h - costheta
+        numerator = -2.0 * params.gamma * c2 * hcth
+        denominator = (d2 + hcth**2) ** 2
+        return numerator / denominator
+
+    def _calc_fc(self, r: np.floating, R: float, D: float) -> float:
+        """Calculate the cutoff function."""
+        if r > R + D:
+            return 0.0
+        if r < R - D:
+            return 1.0
+        return 0.5 * (1.0 - np.sin(np.pi * (r - R) / (2.0 * D)))
+
+    def _calc_fc_d(self, r: np.floating, R: float, D: float) -> float:
+        """Calculate cutoff function derivative with respect to ``r``."""
+        if r > R + D or r < R - D:
+            return 0.0
+        return -0.25 * np.pi / D * np.cos(np.pi * (r - R) / (2.0 * D))
+
+    def _calc_zeta_d(
+        self,
+        rij: np.ndarray,
+        rik: np.ndarray,
+        params: TersoffParameters,
+    ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+        """Calculate the derivatives of ``zeta``.
+
+        Returns
+        -------
+        dri : ndarray of shape (3,), dtype float
+            Derivative with respect to the position of atom ``i``.
+        drj : ndarray of shape (3,), dtype float
+            Derivative with respect to the position of atom ``j``.
+        drk : ndarray of shape (3,), dtype float
+            Derivative with respect to the position of atom ``k``.
+
+        """
+        lam3 = params.lambda3
+        m = params.m
+
+        abs_rij = np.linalg.norm(rij)
+        abs_rik = np.linalg.norm(rik)
+
+        rij_hat = rij / abs_rij
+        rik_hat = rik / abs_rik
+
+        fcik = self._calc_fc(abs_rik, params.R, params.D)
+        dfcik = self._calc_fc_d(abs_rik, params.R, params.D)
+
+        tmp = (lam3 * (abs_rij - abs_rik)) ** m
+        if tmp > _MAX_EXP_ARG:
+            ex_delr = 1.0e30
+        elif tmp < _MIN_EXP_ARG:
+            ex_delr = 0.0
+        else:
+            ex_delr = np.exp(tmp)
+
+        ex_delr_d = m * lam3**m * (abs_rij - abs_rik) ** (m - 1) * ex_delr
+
+        costheta = rij_hat @ rik_hat
+        gijk = self._calc_gijk(costheta, params)
+        gijk_d = self._calc_gijk_d(costheta, params)
+
+        dcosdri, dcosdrj, dcosdrk = self._calc_costheta_d(rij, rik)
+
+        dri = -dfcik * gijk * ex_delr * rik_hat
+        dri += fcik * gijk_d * ex_delr * dcosdri
+        dri += fcik * gijk * ex_delr_d * rik_hat
+        dri -= fcik * gijk * ex_delr_d * rij_hat
+
+        drj = fcik * gijk_d * ex_delr * dcosdrj
+        drj += fcik * gijk * ex_delr_d * rij_hat
+
+        drk = dfcik * gijk * ex_delr * rik_hat
+        drk += fcik * gijk_d * ex_delr * dcosdrk
+        drk -= fcik * gijk * ex_delr_d * rik_hat
+
+        return dri, drj, drk
+
+    def _calc_costheta_d(
+        self,
+        rij: np.ndarray,
+        rik: np.ndarray,
+    ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+        r"""Calculate the derivatives of ``costheta``.
+
+        If
+
+        .. math::
+            \cos \theta = \frac{\mathbf{u} \cdot \mathbf{v}}{u v}
+
+        Then
+
+        .. math::
+            \frac{\partial \cos \theta}{\partial \mathbf{u}}
+            = \frac{\mathbf{v}}{u v}
+            - \frac{\mathbf{u} \cdot \mathbf{v}}{v} \cdot \frac{\mathbf{u}}{u^3}
+            = \frac{\mathbf{v}}{u v} - \frac{\cos \theta}{u^2} \mathbf{u}
+
+        Parameters
+        ----------
+        rij : ndarray of shape (3,), dtype float
+            Vector from atoms ``i`` to ``j``.
+        rik : ndarray of shape (3,), dtype float
+            Vector from atoms ``i`` to ``k``.
+
+        Returns
+        -------
+        dri : ndarray of shape (3,), dtype float
+            Derivative with respect to the position of atom ``i``.
+        drj : ndarray of shape (3,), dtype float
+            Derivative with respect to the position of atom ``j``.
+        drk : ndarray of shape (3,), dtype float
+            Derivative with respect to the position of atom ``k``.
+
+        """
+        abs_rij = np.linalg.norm(rij)
+        abs_rik = np.linalg.norm(rik)
+        costheta = (rij @ rik) / (abs_rij * abs_rik)
+        drj = (rik / abs_rik - costheta * rij / abs_rij) / abs_rij
+        drk = (rij / abs_rij - costheta * rik / abs_rik) / abs_rik
+        dri = -(drj + drk)
+        return dri, drj, drk
diff -pruN 3.24.0-1/ase/calculators/test.py 3.26.0-1/ase/calculators/test.py
--- 3.24.0-1/ase/calculators/test.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/test.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from math import pi
 
 import numpy as np
diff -pruN 3.24.0-1/ase/calculators/tip3p.py 3.26.0-1/ase/calculators/tip3p.py
--- 3.24.0-1/ase/calculators/tip3p.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/tip3p.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """TIP3P potential."""
 
 import numpy as np
diff -pruN 3.24.0-1/ase/calculators/tip4p.py 3.26.0-1/ase/calculators/tip4p.py
--- 3.24.0-1/ase/calculators/tip4p.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/tip4p.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase import units
diff -pruN 3.24.0-1/ase/calculators/turbomole/executor.py 3.26.0-1/ase/calculators/turbomole/executor.py
--- 3.24.0-1/ase/calculators/turbomole/executor.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/turbomole/executor.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Execution of turbomole binaries and scripts:
 define, dscf, grad, ridft, rdgrad, aoforce, jobex, NumForce
diff -pruN 3.24.0-1/ase/calculators/turbomole/parameters.py 3.26.0-1/ase/calculators/turbomole/parameters.py
--- 3.24.0-1/ase/calculators/turbomole/parameters.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/turbomole/parameters.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # type: ignore
 """turbomole parameters management classes and functions"""
 
diff -pruN 3.24.0-1/ase/calculators/turbomole/reader.py 3.26.0-1/ase/calculators/turbomole/reader.py
--- 3.24.0-1/ase/calculators/turbomole/reader.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/turbomole/reader.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Functions to read from control file and from turbomole standard output"""
 
 import os
diff -pruN 3.24.0-1/ase/calculators/turbomole/turbomole.py 3.26.0-1/ase/calculators/turbomole/turbomole.py
--- 3.24.0-1/ase/calculators/turbomole/turbomole.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/turbomole/turbomole.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # type: ignore
 """
 This module defines an ASE interface to Turbomole: http://www.turbomole.com/
diff -pruN 3.24.0-1/ase/calculators/turbomole/writer.py 3.26.0-1/ase/calculators/turbomole/writer.py
--- 3.24.0-1/ase/calculators/turbomole/writer.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/turbomole/writer.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Module containing code to manupulate control file"""
 import subprocess
 
diff -pruN 3.24.0-1/ase/calculators/vasp/__init__.py 3.26.0-1/ase/calculators/vasp/__init__.py
--- 3.24.0-1/ase/calculators/vasp/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/vasp/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from .interactive import VaspInteractive
 from .vasp import Vasp
 from .vasp2 import Vasp2
diff -pruN 3.24.0-1/ase/calculators/vasp/create_input.py 3.26.0-1/ase/calculators/vasp/create_input.py
--- 3.24.0-1/ase/calculators/vasp/create_input.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/vasp/create_input.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Copyright (C) 2008 CSC - Scientific Computing Ltd.
 """This module defines an ASE interface to VASP.
 
@@ -22,11 +24,12 @@ import os
 import shutil
 import warnings
 from os.path import isfile, islink, join
-from typing import List, Sequence, Tuple
+from typing import List, Sequence, TextIO, Tuple, Union
 
 import numpy as np
 
 import ase
+from ase import Atoms
 from ase.calculators.calculator import kpts2ndarray
 from ase.calculators.vasp.setups import get_default_setups
 from ase.config import cfg
@@ -117,22 +120,27 @@ def set_ldau(ldau_param, luj_params, sym
     return ldau_dct
 
 
-def test_nelect_charge_compitability(nelect, charge, nelect_from_ppp):
-    # We need to determine the nelect resulting from a given
-    # charge in any case if it's != 0, but if nelect is
-    # additionally given explicitly, then we need to determine it
-    # even for net charge of 0 to check for conflicts
-    if charge is not None and charge != 0:
+def _calc_nelect_from_charge(
+    nelect: Union[float, None],
+    charge: Union[float, None],
+    nelect_from_ppp: float,
+) -> Union[float, None]:
+    """Determine nelect resulting from a given charge if charge != 0.0.
+
+    If nelect is additionally given explicitly, then we need to determine it
+    even for net charge of 0 to check for conflicts.
+
+    """
+    if charge is not None and charge != 0.0:
         nelect_from_charge = nelect_from_ppp - charge
         if nelect and nelect != nelect_from_charge:
-            raise ValueError('incompatible input parameters: '
-                             f'nelect={nelect}, but charge={charge} '
-                             '(neutral nelect is '
-                             f'{nelect_from_ppp})')
-        print(nelect_from_charge)
+            raise ValueError(
+                'incompatible input parameters: '
+                f'nelect={nelect}, but charge={charge} '
+                f'(neutral nelect is {nelect_from_ppp})'
+            )
         return nelect_from_charge
-    else:
-        return nelect
+    return nelect  # NELECT explicitly given in INCAR (`None` if not given)
 
 
 def get_pp_setup(setup) -> Tuple[dict, Sequence[int]]:
@@ -1496,7 +1504,7 @@ class GenerateVaspInput:
                 raise RuntimeError(msg)
         return ppp_list
 
-    def initialize(self, atoms):
+    def initialize(self, atoms: Atoms) -> None:
         """Initialize a VASP calculation
 
         Constructs the POTCAR file (does not actually write it).
@@ -1534,32 +1542,34 @@ class GenerateVaspInput:
         # Check if the necessary POTCAR files exists and
         # create a list of their paths.
         atomtypes = atoms.get_chemical_symbols()
-        self.symbol_count = []
+        self.symbol_count: list[tuple[str, int]] = []
         for m in special_setups:
-            self.symbol_count.append([atomtypes[m], 1])
-        for m in symbols:
-            self.symbol_count.append([m, symbolcount[m]])
+            self.symbol_count.append((atomtypes[m], 1))
+        for s in symbols:
+            self.symbol_count.append((s, symbolcount[s]))
 
         # create pseudopotential list
-        self.ppp_list = self._build_pp_list(atoms,
-                                            setups=setups,
-                                            special_setups=special_setups)
+        self.ppp_list = self._build_pp_list(
+            atoms,
+            setups=setups,
+            special_setups=special_setups,
+        )
 
         self.converged = None
         self.setups_changed = None
 
-    def default_nelect_from_ppp(self):
+    def default_nelect_from_ppp(self) -> float:
         """ Get default number of electrons from ppp_list and symbol_count
 
         "Default" here means that the resulting cell would be neutral.
         """
-        symbol_valences = []
+        symbol_valences: list[tuple[str, float]] = []
         for filename in self.ppp_list:
             with open_potcar(filename=filename) as ppp_file:
                 r = read_potcar_numbers_of_electrons(ppp_file)
                 symbol_valences.extend(r)
         assert len(self.symbol_count) == len(symbol_valences)
-        default_nelect = 0
+        default_nelect = 0.0
         for ((symbol1, count),
              (symbol2, valence)) in zip(self.symbol_count, symbol_valences):
             assert symbol1 == symbol2
@@ -1639,7 +1649,7 @@ class GenerateVaspInput:
 
         if 'charge' in self.input_params and self.input_params[
                 'charge'] is not None:
-            nelect_val = test_nelect_charge_compitability(
+            nelect_val = _calc_nelect_from_charge(
                 self.float_params['nelect'],
                 self.input_params['charge'],
                 self.default_nelect_from_ppp())
@@ -1780,6 +1790,8 @@ class GenerateVaspInput:
                 raise ValueError("KSPACING value {} is not allowable. "
                                  "Please use None or a positive number."
                                  "".format(self.float_params['kspacing']))
+        if self.input_params['kpts'] is None:
+            return
 
         kpointstring = format_kpoints(
             kpts=self.input_params['kpts'],
@@ -2073,21 +2085,27 @@ def open_potcar(filename):
         raise ValueError(f'Invalid POTCAR filename: "{filename}"')
 
 
-def read_potcar_numbers_of_electrons(file_obj):
-    """ Read list of tuples (atomic symbol, number of valence electrons)
-    for each atomtype from a POTCAR file."""
-    nelect = []
-    lines = file_obj.readlines()
+def read_potcar_numbers_of_electrons(fd: TextIO, /) -> list[tuple[str, float]]:
+    """Read number of valence electrons for each atomtype from a POTCAR file.
+
+    Returns
+    -------
+    list[tuple[str, float]]
+        List of (atomic symbol, number of valence electrons).
+
+    """
+    nelect: list[tuple[str, float]] = []
+    lines = fd.readlines()
     for n, line in enumerate(lines):
         if 'TITEL' in line:
             symbol = line.split('=')[1].split()[1].split('_')[0].strip()
-            valence = float(
-                lines[n + 4].split(';')[1].split('=')[1].split()[0].strip())
-            nelect.append((symbol, valence))
+            linep4 = lines[n + 4]
+            zval = float(linep4.split(';')[1].split('=')[1].split()[0].strip())
+            nelect.append((symbol, zval))
     return nelect
 
 
-def count_symbols(atoms, exclude=()):
+def count_symbols(atoms: Atoms, exclude=()) -> tuple[list[str], dict[str, int]]:
     """Count symbols in atoms object, excluding a set of indices
 
     Parameters:
@@ -2108,8 +2126,8 @@ def count_symbols(atoms, exclude=()):
     >>> count_symbols(atoms, exclude=(1, 2, 3))
     (['Na', 'Cl'], {'Na': 3, 'Cl': 2})
     """
-    symbols = []
-    symbolcount = {}
+    symbols: list[str] = []
+    symbolcount: dict[str, int] = {}
     for m, symbol in enumerate(atoms.symbols):
         if m in exclude:
             continue
diff -pruN 3.24.0-1/ase/calculators/vasp/interactive.py 3.26.0-1/ase/calculators/vasp/interactive.py
--- 3.24.0-1/ase/calculators/vasp/interactive.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/vasp/interactive.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import time
 from subprocess import PIPE, Popen
diff -pruN 3.24.0-1/ase/calculators/vasp/setups.py 3.26.0-1/ase/calculators/vasp/setups.py
--- 3.24.0-1/ase/calculators/vasp/setups.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/vasp/setups.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import copy
 
 _setups_defaults = {
diff -pruN 3.24.0-1/ase/calculators/vasp/vasp.py 3.26.0-1/ase/calculators/vasp/vasp.py
--- 3.24.0-1/ase/calculators/vasp/vasp.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/vasp/vasp.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Copyright (C) 2008 CSC - Scientific Computing Ltd.
 """This module defines an ASE interface to VASP.
 
@@ -1196,7 +1198,7 @@ class Vasp(GenerateVaspInput, Calculator
                     i_freq.append(float(data[-2]))
         return freq, i_freq
 
-    def _read_massweighted_hessian_xml(self) -> np.ndarray:
+    def _read_massweighted_hessian_xml(self):
         """Read the Mass Weighted Hessian from vasprun.xml.
 
         Returns:
diff -pruN 3.24.0-1/ase/calculators/vasp/vasp_auxiliary.py 3.26.0-1/ase/calculators/vasp/vasp_auxiliary.py
--- 3.24.0-1/ase/calculators/vasp/vasp_auxiliary.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/vasp/vasp_auxiliary.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import re
 
@@ -87,12 +89,17 @@ class VaspChargeDensity:
             self.augdiff = ''
             while True:
                 try:
-                    atoms = aiv.read_vasp(fd)
+                    atoms = aiv.read_vasp_configuration(fd)
                 except (KeyError, RuntimeError, ValueError):
                     # Probably an empty line, or we tried to read the
                     # augmentation occupancies in CHGCAR
                     break
+
+                # Note: We continue reading from the same file, and
+                # this relies on read_vasp() to read no more lines
+                # than it currently does.
                 fd.readline()
+
                 ngr = fd.readline().split()
                 ng = (int(ngr[0]), int(ngr[1]), int(ngr[2]))
                 chg = np.empty(ng)
diff -pruN 3.24.0-1/ase/calculators/vasp/vasp_data.py 3.26.0-1/ase/calculators/vasp/vasp_data.py
--- 3.24.0-1/ase/calculators/vasp/vasp_data.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/vasp/vasp_data.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 
 
diff -pruN 3.24.0-1/ase/calculators/vdwcorrection.py 3.26.0-1/ase/calculators/vdwcorrection.py
--- 3.24.0-1/ase/calculators/vdwcorrection.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/calculators/vdwcorrection.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """van der Waals correction schemes for DFT"""
 import numpy as np
 from scipy.special import erfc, erfinv
diff -pruN 3.24.0-1/ase/cell.py 3.26.0-1/ase/cell.py
--- 3.24.0-1/ase/cell.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cell.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import Mapping, Sequence, Union
 
 import numpy as np
@@ -328,14 +330,19 @@ class Cell:
         permuted = Cell(self[permutation][:, permutation])
         return permuted
 
-    def standard_form(self):
-        """Rotate axes such that unit cell is lower triangular. The cell
+    def standard_form(self, form='lower'):
+        """Rotate axes such that unit cell is lower/upper triangular. The cell
         handedness is preserved.
 
         A lower-triangular cell with positive diagonal entries is a canonical
         (i.e. unique) description. For a left-handed cell the diagonal entries
         are negative.
 
+        Parameters:
+
+        form: str
+            'lower' or 'upper' triangular form. The default is 'lower'.
+
         Returns:
 
         rcell: the standardized cell object
@@ -353,9 +360,16 @@ class Cell:
         # Q is an orthogonal matrix and L is a lower triangular matrix. The
         # decomposition is a unique description if the diagonal elements are
         # all positive (negative for a left-handed cell).
-        Q, L = np.linalg.qr(self.T)
-        Q = Q.T
-        L = L.T
+        if form == 'lower':
+            Q, L = np.linalg.qr(self.T)
+            Q = Q.T
+            L = L.T
+        elif form == 'upper':
+            Q, L = np.linalg.qr(self.T[::-1, ::-1])
+            Q = Q.T[::-1, ::-1]
+            L = L.T[::-1, ::-1]
+        else:
+            raise ValueError('form must be either "lower" or "upper"')
 
         # correct the signs of the diagonal elements
         signs = np.sign(np.diag(L))
diff -pruN 3.24.0-1/ase/cli/band_structure.py 3.26.0-1/ase/cli/band_structure.py
--- 3.24.0-1/ase/cli/band_structure.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/band_structure.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
diff -pruN 3.24.0-1/ase/cli/build.py 3.26.0-1/ase/cli/build.py
--- 3.24.0-1/ase/cli/build.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/build.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
@@ -18,7 +20,7 @@ class CLICommand:
         ase build <formula> ...
 
     where <formula> must be one of the formulas known to ASE
-    (see here: https://wiki.fysik.dtu.dk/ase/ase/build/build.html#molecules).
+    (see here: https://ase-lib.org/ase/build/build.html#molecules).
 
     Bulk:
 
diff -pruN 3.24.0-1/ase/cli/complete.py 3.26.0-1/ase/cli/complete.py
--- 3.24.0-1/ase/cli/complete.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/complete.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,6 @@
 #!/usr/bin/env python3
+# fmt: off
+
 """Bash completion for ase.
 
 Put this in your .bashrc::
@@ -73,10 +75,6 @@ commands = {
         ['--files', '-v', '--verbose', '--formats', '--calculators'],
     'nebplot':
         ['--nimages', '--share-x', '--share-y'],
-    'nomad-get':
-        [],
-    'nomad-upload':
-        ['-t', '--token', '-n', '--no-save-token', '-0', '--dry-run'],
     'reciprocal':
         [],
     'run':
diff -pruN 3.24.0-1/ase/cli/completion.py 3.26.0-1/ase/cli/completion.py
--- 3.24.0-1/ase/cli/completion.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/completion.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """TAB-completion sub-command and update helper funtion.
 
 Run this when ever options are changed::
@@ -54,6 +56,9 @@ def update(path: Path,
             dct[command].extend(arg for arg in args
                                 if arg.startswith('-'))
 
+        def add_argument_group(self, name):
+            return self
+
         def add_mutually_exclusive_group(self, required=False):
             return self
 
diff -pruN 3.24.0-1/ase/cli/convert.py 3.26.0-1/ase/cli/convert.py
--- 3.24.0-1/ase/cli/convert.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/convert.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
diff -pruN 3.24.0-1/ase/cli/db.py 3.26.0-1/ase/cli/db.py
--- 3.24.0-1/ase/cli/db.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/db.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
@@ -31,7 +33,7 @@ class CLICommand:
         2.2<bandgap<4.1
         Cu>=10
 
-    See also: https://wiki.fysik.dtu.dk/ase/ase/db/db.html.
+    See also: https://ase-lib.org/ase/db/db.html.
     """
 
     @staticmethod
diff -pruN 3.24.0-1/ase/cli/diff.py 3.26.0-1/ase/cli/diff.py
--- 3.24.0-1/ase/cli/diff.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/diff.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
diff -pruN 3.24.0-1/ase/cli/dimensionality.py 3.26.0-1/ase/cli/dimensionality.py
--- 3.24.0-1/ase/cli/dimensionality.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/dimensionality.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
diff -pruN 3.24.0-1/ase/cli/exec.py 3.26.0-1/ase/cli/exec.py
--- 3.24.0-1/ase/cli/exec.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/exec.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
diff -pruN 3.24.0-1/ase/cli/find.py 3.26.0-1/ase/cli/find.py
--- 3.24.0-1/ase/cli/find.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/find.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
@@ -9,7 +11,7 @@ class CLICommand:
 
     Search through files known to ASE applying a query to filter the results.
 
-    See https://wiki.fysik.dtu.dk/ase/ase/db/db.html#querying for more
+    See https://ase-lib.org/ase/db/db.html#querying for more
     informations on how to construct the query string.
     """
 
diff -pruN 3.24.0-1/ase/cli/info.py 3.26.0-1/ase/cli/info.py
--- 3.24.0-1/ase/cli/info.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/info.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
diff -pruN 3.24.0-1/ase/cli/main.py 3.26.0-1/ase/cli/main.py
--- 3.24.0-1/ase/cli/main.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/main.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import argparse
 import textwrap
 from importlib import import_module
@@ -28,8 +30,6 @@ commands = [
     ('ulm', 'ase.cli.ulm'),
     ('find', 'ase.cli.find'),
     ('nebplot', 'ase.cli.nebplot'),
-    ('nomad-upload', 'ase.cli.nomad'),
-    ('nomad-get', 'ase.cli.nomadget'),
     ('convert', 'ase.cli.convert'),
     ('reciprocal', 'ase.cli.reciprocal'),
     ('completion', 'ase.cli.completion'),
diff -pruN 3.24.0-1/ase/cli/nebplot.py 3.26.0-1/ase/cli/nebplot.py
--- 3.24.0-1/ase/cli/nebplot.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/nebplot.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
diff -pruN 3.24.0-1/ase/cli/nomad.py 3.26.0-1/ase/cli/nomad.py
--- 3.24.0-1/ase/cli/nomad.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/nomad.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,75 +0,0 @@
-# Note:
-# Try to avoid module level import statements here to reduce
-# import time during CLI execution
-
-
-class CLICommand:
-    """Upload files to NOMAD.
-
-    Upload all data within specified folders to the Nomad repository
-    using authentication token given by the --token option or,
-    if no token is given, the token stored in ~/.ase/nomad-token.
-
-    To get an authentication token, you create a Nomad repository account
-    and use the 'Uploads' button on that page while logged in:
-
-      https://repository.nomad-coe.eu/
-    """
-
-    @staticmethod
-    def add_arguments(parser):
-        parser.add_argument('folders', nargs='*', metavar='folder')
-        parser.add_argument('-t', '--token',
-                            help='Use given authentication token and save '
-                            'it to ~/.ase/nomad-token unless '
-                            '--no-save-token')
-        parser.add_argument('-n', '--no-save-token', action='store_true',
-                            help='do not save the token if given')
-        parser.add_argument('-0', '--dry-run', action='store_true',
-                            help='print command that would upload files '
-                            'without uploading anything')
-
-    @staticmethod
-    def run(args):
-        import os
-        import os.path as op
-        import subprocess
-
-        dotase = op.expanduser('~/.ase')
-        tokenfile = op.join(dotase, 'nomad-token')
-
-        if args.token:
-            token = args.token
-            if not args.no_save_token:
-                if not op.isdir(dotase):
-                    os.mkdir(dotase)
-                with open(tokenfile, 'w') as fd:
-                    print(token, file=fd)
-                os.chmod(tokenfile, 0o600)
-                print('Wrote token to', tokenfile)
-        else:
-            try:
-                with open(tokenfile) as fd:
-                    token = fd.readline().strip()
-            except OSError as err:  # py2/3 discrepancy
-                from ase.cli.main import CLIError
-                msg = ('Could not find authentication token in {}.  '
-                       'Use the --token option to specify a token.  '
-                       'Original error: {}'
-                       .format(tokenfile, err))
-                raise CLIError(msg)
-
-        cmd = ('tar cf - {} | '
-               'curl -XPUT -# -HX-Token:{} '
-               '-N -F file=@- http://nomad-repository.eu:8000 | '
-               'xargs echo').format(' '.join(args.folders), token)
-
-        if not args.folders:
-            print('No folders specified -- another job well done!')
-        elif args.dry_run:
-            print(cmd)
-        else:
-            print('Uploading {} folder{} ...'
-                  .format(len(args.folders),
-                          's' if len(args.folders) != 1 else ''))
-            subprocess.check_call(cmd, shell=True)
diff -pruN 3.24.0-1/ase/cli/nomadget.py 3.26.0-1/ase/cli/nomadget.py
--- 3.24.0-1/ase/cli/nomadget.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/nomadget.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,28 +0,0 @@
-# Note:
-# Try to avoid module level import statements here to reduce
-# import time during CLI execution
-
-
-class CLICommand:
-    """Get calculations from NOMAD and write to JSON files.
-
-    ...
-    """
-
-    @staticmethod
-    def add_arguments(p):
-        p.add_argument('uri', nargs='+', metavar='nmd://<hash>',
-                       help='URIs to get')
-
-    @staticmethod
-    def run(args):
-        import json
-
-        from ase.nomad import download
-        for uri in args.uri:
-            calculation = download(uri)
-            identifier = calculation.hash.replace('/', '.')
-            fname = f'nmd.{identifier}.nomad-json'
-            with open(fname, 'w') as fd:
-                json.dump(calculation, fd)
-            print(uri)
diff -pruN 3.24.0-1/ase/cli/reciprocal.py 3.26.0-1/ase/cli/reciprocal.py
--- 3.24.0-1/ase/cli/reciprocal.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/reciprocal.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
diff -pruN 3.24.0-1/ase/cli/run.py 3.26.0-1/ase/cli/run.py
--- 3.24.0-1/ase/cli/run.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/run.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import sys
 from typing import Any, Dict
 
diff -pruN 3.24.0-1/ase/cli/template.py 3.26.0-1/ase/cli/template.py
--- 3.24.0-1/ase/cli/template.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/template.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import string
 
 import numpy as np
diff -pruN 3.24.0-1/ase/cli/ulm.py 3.26.0-1/ase/cli/ulm.py
--- 3.24.0-1/ase/cli/ulm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cli/ulm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Note:
 # Try to avoid module level import statements here to reduce
 # import time during CLI execution
diff -pruN 3.24.0-1/ase/cluster/__init__.py 3.26.0-1/ase/cluster/__init__.py
--- 3.24.0-1/ase/cluster/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Module for creating clusters."""
 
 from ase.cluster.cluster import Cluster
diff -pruN 3.24.0-1/ase/cluster/base.py 3.26.0-1/ase/cluster/base.py
--- 3.24.0-1/ase/cluster/base.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/base.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 
diff -pruN 3.24.0-1/ase/cluster/cluster.py 3.26.0-1/ase/cluster/cluster.py
--- 3.24.0-1/ase/cluster/cluster.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/cluster.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import math
 
 import numpy as np
diff -pruN 3.24.0-1/ase/cluster/compounds.py 3.26.0-1/ase/cluster/compounds.py
--- 3.24.0-1/ase/cluster/compounds.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/compounds.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.cluster.cubic import SimpleCubicFactory
diff -pruN 3.24.0-1/ase/cluster/cubic.py 3.26.0-1/ase/cluster/cubic.py
--- 3.24.0-1/ase/cluster/cubic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/cubic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Function-like objects that creates cubic clusters.
 """
diff -pruN 3.24.0-1/ase/cluster/decahedron.py 3.26.0-1/ase/cluster/decahedron.py
--- 3.24.0-1/ase/cluster/decahedron.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/decahedron.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/cluster/factory.py 3.26.0-1/ase/cluster/factory.py
--- 3.24.0-1/ase/cluster/factory.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/factory.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import List, Optional
 
 import numpy as np
diff -pruN 3.24.0-1/ase/cluster/hexagonal.py 3.26.0-1/ase/cluster/hexagonal.py
--- 3.24.0-1/ase/cluster/hexagonal.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/hexagonal.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Function-like objects that creates cubic clusters.
 """
diff -pruN 3.24.0-1/ase/cluster/icosahedron.py 3.26.0-1/ase/cluster/icosahedron.py
--- 3.24.0-1/ase/cluster/icosahedron.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/icosahedron.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/cluster/octahedron.py 3.26.0-1/ase/cluster/octahedron.py
--- 3.24.0-1/ase/cluster/octahedron.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/octahedron.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Function-like objects that creates cubic clusters.
 """
diff -pruN 3.24.0-1/ase/cluster/util.py 3.26.0-1/ase/cluster/util.py
--- 3.24.0-1/ase/cluster/util.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/util.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.data import atomic_numbers, chemical_symbols, reference_states
 
 
diff -pruN 3.24.0-1/ase/cluster/wulff.py 3.26.0-1/ase/cluster/wulff.py
--- 3.24.0-1/ase/cluster/wulff.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/cluster/wulff.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 delta = 1e-10
diff -pruN 3.24.0-1/ase/codes.py 3.26.0-1/ase/codes.py
--- 3.24.0-1/ase/codes.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/codes.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from dataclasses import dataclass
 
 # Note: There could be more than one "calculator" for any given code;
diff -pruN 3.24.0-1/ase/collections/collection.py 3.26.0-1/ase/collections/collection.py
--- 3.24.0-1/ase/collections/collection.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/collections/collection.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os.path as op
 
 from ase.db.row import AtomsRow
diff -pruN 3.24.0-1/ase/collections/create.py 3.26.0-1/ase/collections/create.py
--- 3.24.0-1/ase/collections/create.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/collections/create.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 
 import ase.db
diff -pruN 3.24.0-1/ase/config.py 3.26.0-1/ase/config.py
--- 3.24.0-1/ase/config.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/config.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import configparser
 import os
 import shlex
diff -pruN 3.24.0-1/ase/constraints.py 3.26.0-1/ase/constraints.py
--- 3.24.0-1/ase/constraints.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/constraints.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Constraints"""
 from typing import Sequence
 from warnings import warn
diff -pruN 3.24.0-1/ase/data/__init__.py 3.26.0-1/ase/data/__init__.py
--- 3.24.0-1/ase/data/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.data.vdw import vdw_radii
diff -pruN 3.24.0-1/ase/data/cccbdb_ip.py 3.26.0-1/ase/data/cccbdb_ip.py
--- 3.24.0-1/ase/data/cccbdb_ip.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/cccbdb_ip.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """
 Experimental ionization energies from CCCBDB at
diff -pruN 3.24.0-1/ase/data/cohesive_energies.py 3.26.0-1/ase/data/cohesive_energies.py
--- 3.24.0-1/ase/data/cohesive_energies.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/cohesive_energies.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 # http://metal.elte.hu/~groma/Anyagtudomany/kittel.pdf
diff -pruN 3.24.0-1/ase/data/colors.py 3.26.0-1/ase/data/colors.py
--- 3.24.0-1/ase/data/colors.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/colors.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 import numpy as np
 
diff -pruN 3.24.0-1/ase/data/dbh24.py 3.26.0-1/ase/data/dbh24.py
--- 3.24.0-1/ase/data/dbh24.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/dbh24.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """
 The following contains a database of 24 gas-phase reaction barrier heights for small molecules.
diff -pruN 3.24.0-1/ase/data/extra_molecules.py 3.26.0-1/ase/data/extra_molecules.py
--- 3.24.0-1/ase/data/extra_molecules.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/extra_molecules.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """
 Database of molecules outside the G2_1 set
diff -pruN 3.24.0-1/ase/data/g2.py 3.26.0-1/ase/data/g2.py
--- 3.24.0-1/ase/data/g2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/g2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """The following contains a database of small molecules
 
 Data for the G2/97 database are from
diff -pruN 3.24.0-1/ase/data/g2_1.py 3.26.0-1/ase/data/g2_1.py
--- 3.24.0-1/ase/data/g2_1.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/g2_1.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """
 The following contains a database of small molecules
diff -pruN 3.24.0-1/ase/data/g2_2.py 3.26.0-1/ase/data/g2_2.py
--- 3.24.0-1/ase/data/g2_2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/g2_2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """
 The following contains a database of small molecules
diff -pruN 3.24.0-1/ase/data/isotopes.py 3.26.0-1/ase/data/isotopes.py
--- 3.24.0-1/ase/data/isotopes.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/isotopes.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Isotope data extracted from NIST public website.
 
 Source data has been compiled by NIST:
diff -pruN 3.24.0-1/ase/data/pubchem.py 3.26.0-1/ase/data/pubchem.py
--- 3.24.0-1/ase/data/pubchem.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/pubchem.py	2025-08-12 11:26:23.000000000 +0000
@@ -2,7 +2,7 @@ import json
 import urllib.request
 import warnings
 from collections import namedtuple
-from io import BytesIO, StringIO
+from io import StringIO
 from urllib.error import HTTPError, URLError
 
 from ase.io import read
@@ -28,7 +28,7 @@ class PubchemData:
         return self.data
 
 
-def search_pubchem_raw(search, field, silent=False, mock_test=False):
+def search_pubchem_raw(search, field, silent=False):
     """
     A helper function for searching pubchem.
 
@@ -49,33 +49,27 @@ def search_pubchem_raw(search, field, si
         data (str):
             a string containing the raw response from pubchem.
     """
-    if mock_test:  # for testing only
-        r = BytesIO(test_output)
-    else:
-        suffix = 'sdf?record_type=3d'
-
-        url = (
-            f'{base_url}/{field}/{search!s}/{suffix}'
-            if field == 'conformers'
-            else f'{base_url}/compound/{field}/{search!s}/{suffix}'
-        )
-        try:
-            r = urllib.request.urlopen(url)
-        except HTTPError as e:
-            raise ValueError(
-                f'the search term {search} could not be found for the field '
-                f'{field}'
-            ) from e
-        except URLError as e:
-            raise ValueError(
-                'Couldn\'t reach the pubchem servers, check'
-                ' your internet connection'
-            ) from e
+    suffix = 'sdf?record_type=3d'
+
+    url = (
+        f'{base_url}/{field}/{search!s}/{suffix}'
+        if field == 'conformers'
+        else f'{base_url}/compound/{field}/{search!s}/{suffix}'
+    )
+    try:
+        r = urllib.request.urlopen(url)
+    except HTTPError as e:
+        raise ValueError(
+            f'the search term {search} could not be found for the field {field}'
+        ) from e
+    except URLError as e:
+        raise ValueError(
+            "Couldn't reach the pubchem servers, check your internet connection"
+        ) from e
 
     # check if there are confomers and warn them if there are
     if field != 'conformers' and not silent:
-        conformer_ids = available_conformer_search(search, field,
-                                                   mock_test=mock_test)
+        conformer_ids = available_conformer_search(search, field)
         if len(conformer_ids) > 1:
             warnings.warn(
                 f'The structure "{search}" has more than one conformer in '
@@ -105,8 +99,7 @@ def parse_pubchem_raw(data):
 
     """
     if 'PUBCHEM_COMPOUND_CID' not in data:
-        raise Exception('There was a problem with the data returned by '
-                        'PubChem')
+        raise Exception('There was a problem with the data returned by PubChem')
     f_like = StringIO(data)
     atoms = read(f_like, format='sdf')
 
@@ -133,7 +126,7 @@ def parse_pubchem_raw(data):
         # the first entry just contains the number of atoms with charges
         charges = pubchem_data['PUBCHEM_MMFF94_PARTIAL_CHARGES'][1:]
         # each subsequent entry contains the index and charge of the atoms
-        atom_charges = [0.] * len(atoms)
+        atom_charges = [0.0] * len(atoms)
         for charge in charges:
             i, charge = charge.split()
             # indices start at 1
@@ -142,8 +135,9 @@ def parse_pubchem_raw(data):
     return atoms, pubchem_data
 
 
-def analyze_input(name=None, cid=None, smiles=None, conformer=None,
-                  silent=False):
+def analyze_input(
+    name=None, cid=None, smiles=None, conformer=None, silent=False
+):
     """
     helper function to translate keyword arguments from intialization
     and searching into the search and field that is being asked for
@@ -162,23 +156,31 @@ def analyze_input(name=None, cid=None, s
     input_fields = ['name', 'cid', 'smiles', 'conformers']
 
     if inputs_check.count(True) > 1:
-        raise ValueError('Only one search term my be entered a time.'
-                         ' Please pass in only one of the following: '
-                         'name, cid, smiles, confomer')
-    elif inputs_check.count(True) == 1:
-        # Figure out which input has been passed in
-        index = inputs_check.index(True)
-        field = input_fields[index]
-        search = inputs[index]
-    else:
-        raise ValueError('No search was entered.'
-                         ' Please pass in only one of the following: '
-                         'name, cid, smiles, confomer')
+        raise ValueError(
+            'Only one search term my be entered a time.'
+            ' Please pass in only one of the following: '
+            'name, cid, smiles, confomer'
+        )
+    if inputs_check.count(True) == 0:
+        raise ValueError(
+            'No search was entered.'
+            ' Please pass in only one of the following: '
+            'name, cid, smiles, confomer'
+        )
+
+    # Figure out which input has been passed in
+    index = inputs_check.index(True)
+    field = input_fields[index]
+    search = inputs[index]
+
+    # convert hash (triple bond) to hex for URL
+    if isinstance(search, str):
+        search = search.replace('#', '%23')
 
     return PubchemSearch(search, field)
 
 
-def available_conformer_search(search, field, mock_test=False):
+def available_conformer_search(search, field) -> list:
     """
     Helper function to get the conformer IDs. This searches pubchem for
     the conformers of a given structure and returns all the confomer ids
@@ -204,27 +206,24 @@ def available_conformer_search(search, f
     """
     suffix = 'conformers/JSON'
     url = f'{base_url}/compound/{field}/{search!s}/{suffix}'
-    if mock_test:
-        r = BytesIO(test_conformer_output)
-    else:
-        try:
-            r = urllib.request.urlopen(url)
-        except HTTPError as e:
-            err = ValueError(
-                f'the search term {search} could not be found for the field '
-                f'{field}'
-            )
-            raise err from e
-        except URLError as e:
-            err = ValueError('Couldn\'t reach the pubchem servers, check'
-                             ' your internet connection')
-            raise err from e
+    try:
+        r = urllib.request.urlopen(url)
+    except HTTPError as e:
+        err = ValueError(
+            f'the search term {search} could not be found for the field {field}'
+        )
+        raise err from e
+    except URLError as e:
+        err = ValueError(
+            "Couldn't reach the pubchem servers, check your internet connection"
+        )
+        raise err from e
     record = r.read().decode('utf-8')
     record = json.loads(record)
     return record['InformationList']['Information'][0]['ConformerID']
 
 
-def pubchem_search(*args, mock_test=False, **kwargs):
+def pubchem_search(*args, **kwargs) -> PubchemData:
     """
     Search PubChem for the field and search input on the argument passed in
     returning a PubchemData object. Note that only one argument may be passed
@@ -245,14 +244,13 @@ def pubchem_search(*args, mock_test=Fals
             a pubchem data object containing the information on the
             requested entry
     """
-
     search, field = analyze_input(*args, **kwargs)
-    raw_pubchem = search_pubchem_raw(search, field, mock_test=mock_test)
+    raw_pubchem = search_pubchem_raw(search, field)
     atoms, data = parse_pubchem_raw(raw_pubchem)
     return PubchemData(atoms, data)
 
 
-def pubchem_conformer_search(*args, mock_test=False, **kwargs):
+def pubchem_conformer_search(*args, **kwargs) -> list:
     """
     Search PubChem for all the conformers of a given compound.
     Note that only one argument may be passed in at a time.
@@ -265,15 +263,9 @@ def pubchem_conformer_search(*args, mock
             a list containing the PubchemData objects of all the conformers
             for your search
     """
-
     search, field = analyze_input(*args, **kwargs)
-
-    conformer_ids = available_conformer_search(search, field,
-                                               mock_test=mock_test)
-    return [
-        pubchem_search(mock_test=mock_test, conformer=id_)
-        for id_ in conformer_ids
-    ]
+    conformer_ids = available_conformer_search(search, field)
+    return [pubchem_search(conformer=id_) for id_ in conformer_ids]
 
 
 def pubchem_atoms_search(*args, **kwargs):
@@ -309,7 +301,3 @@ def pubchem_atoms_conformer_search(*args
     conformers = pubchem_conformer_search(*args, **kwargs)
     conformers = [conformer.get_atoms() for conformer in conformers]
     return conformers
-
-
-test_output = b'222\n  -OEChem-10071914343D\n\n  4  3  0     0  0  0  0  0  0999 V2000\n    0.0000    0.0000    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0\n   -0.4417    0.2906    0.8711 H   0  0  0  0  0  0  0  0  0  0  0  0\n    0.7256    0.6896   -0.1907 H   0  0  0  0  0  0  0  0  0  0  0  0\n    0.4875   -0.8701    0.2089 H   0  0  0  0  0  0  0  0  0  0  0  0\n  1  2  1  0  0  0  0\n  1  3  1  0  0  0  0\n  1  4  1  0  0  0  0\nM  END\n> <PUBCHEM_COMPOUND_CID>\n222\n\n> <PUBCHEM_CONFORMER_RMSD>\n0.4\n\n> <PUBCHEM_CONFORMER_DIVERSEORDER>\n1\n\n> <PUBCHEM_MMFF94_PARTIAL_CHARGES>\n4\n1 -1.08\n2 0.36\n3 0.36\n4 0.36\n\n> <PUBCHEM_EFFECTIVE_ROTOR_COUNT>\n0\n\n> <PUBCHEM_PHARMACOPHORE_FEATURES>\n1\n1 1 cation\n\n> <PUBCHEM_HEAVY_ATOM_COUNT>\n1\n\n> <PUBCHEM_ATOM_DEF_STEREO_COUNT>\n0\n\n> <PUBCHEM_ATOM_UDEF_STEREO_COUNT>\n0\n\n> <PUBCHEM_BOND_DEF_STEREO_COUNT>\n0\n\n> <PUBCHEM_BOND_UDEF_STEREO_COUNT>\n0\n\n> <PUBCHEM_ISOTOPIC_ATOM_COUNT>\n0\n\n> <PUBCHEM_COMPONENT_COUNT>\n1\n\n> <PUBCHEM_CACTVS_TAUTO_COUNT>\n1\n\n> <PUBCHEM_CONFORMER_ID>\n000000DE00000001\n\n> <PUBCHEM_MMFF94_ENERGY>\n0\n\n> <PUBCHEM_FEATURE_SELFOVERLAP>\n5.074\n\n> <PUBCHEM_SHAPE_FINGERPRINT>\n260 1 18410856563934756871\n\n> <PUBCHEM_SHAPE_MULTIPOLES>\n15.6\n0.51\n0.51\n0.51\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n\n> <PUBCHEM_SHAPE_SELFOVERLAP>\n14.89\n\n> <PUBCHEM_SHAPE_VOLUME>\n15.6\n\n> <PUBCHEM_COORDINATE_TYPE>\n2\n5\n10\n\n$$$$\n'  # noqa
-test_conformer_output = b'{\n  "InformationList": {\n    "Information": [\n      {\n        "CID": 222,\n        "ConformerID": [\n          "000000DE00000001"\n        ]\n      }\n    ]\n  }\n}\n'  # noqa
diff -pruN 3.24.0-1/ase/data/s22.py 3.26.0-1/ase/data/s22.py
--- 3.24.0-1/ase/data/s22.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/s22.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """
 The following contains the S22 and s26 databases of weakly interacting dimers and complexes
diff -pruN 3.24.0-1/ase/data/vdw.py 3.26.0-1/ase/data/vdw.py
--- 3.24.0-1/ase/data/vdw.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/vdw.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ Van der Waals radii in [A] taken from
 http://www.webelements.com/periodicity/van_der_waals_radius/
 and the references given there.
diff -pruN 3.24.0-1/ase/data/vdw_alvarez.py 3.26.0-1/ase/data/vdw_alvarez.py
--- 3.24.0-1/ase/data/vdw_alvarez.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/data/vdw_alvarez.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """ Van der Waals radii in [A] taken from:
 A cartography of the van der Waals territories
diff -pruN 3.24.0-1/ase/db/app.py 3.26.0-1/ase/db/app.py
--- 3.24.0-1/ase/db/app.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/app.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """WSGI Flask-app for browsing a database.
 
 ::
diff -pruN 3.24.0-1/ase/db/cli.py 3.26.0-1/ase/db/cli.py
--- 3.24.0-1/ase/db/cli.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/cli.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import json
 import sys
 from collections import defaultdict
@@ -291,7 +293,7 @@ def row2str(row) -> str:
     fmt = ('   {0}|     {1}|{2[0]:>11}|{2[1]:>11}|{2[2]:>11}|' +
            '{3:>10}|{4:>10}')
     for p, axis, L, A in zip(row.pbc, t['cell'], t['lengths'], t['angles']):
-        S.append(fmt.format(c, [' no', 'yes'][p], axis, L, A))
+        S.append(fmt.format(c, [' no', 'yes'][int(p)], axis, L, A))
         c += 1
     S.append('')
 
diff -pruN 3.24.0-1/ase/db/convert.py 3.26.0-1/ase/db/convert.py
--- 3.24.0-1/ase/db/convert.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/convert.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import optparse
 import os
 
diff -pruN 3.24.0-1/ase/db/core.py 3.26.0-1/ase/db/core.py
--- 3.24.0-1/ase/db/core.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/core.py	2025-08-12 11:26:23.000000000 +0000
@@ -50,8 +50,10 @@ class KeyDescription:
 
     def __repr__(self):
         cls = type(self).__name__
-        return (f'{cls}({self.key!r}, {self.shortdesc!r}, {self.longdesc!r}, '
-                f'unit={self.unit!r})')
+        return (
+            f'{cls}({self.key!r}, {self.shortdesc!r}, {self.longdesc!r}, '
+            f'unit={self.unit!r})'
+        )
 
     # The templates like to sort key descriptions by shortdesc.
     def __eq__(self, other):
@@ -63,24 +65,31 @@ class KeyDescription:
 
 def get_key_descriptions():
     KD = KeyDescription
-    return {keydesc.key: keydesc for keydesc in [
-        KD('id', 'ID', 'Uniqe row ID'),
-        KD('age', 'Age', 'Time since creation'),
-        KD('formula', 'Formula', 'Chemical formula'),
-        KD('pbc', 'PBC', 'Periodic boundary conditions'),
-        KD('user', 'Username'),
-        KD('calculator', 'Calculator', 'ASE-calculator name'),
-        KD('energy', 'Energy', 'Total energy', unit='eV'),
-        KD('natoms', 'Number of atoms'),
-        KD('fmax', 'Maximum force', unit='eV/Å'),
-        KD('smax', 'Maximum stress', 'Maximum stress on unit cell',
-           unit='eV/Å³'),
-        KD('charge', 'Charge', 'Net charge in unit cell', unit='|e|'),
-        KD('mass', 'Mass', 'Sum of atomic masses in unit cell', unit='au'),
-        KD('magmom', 'Magnetic moment', unit='μ_B'),
-        KD('unique_id', 'Unique ID', 'Random (unique) ID'),
-        KD('volume', 'Volume', 'Volume of unit cell', unit='Å³')
-    ]}
+    return {
+        keydesc.key: keydesc
+        for keydesc in [
+            KD('id', 'ID', 'Uniqe row ID'),
+            KD('age', 'Age', 'Time since creation'),
+            KD('formula', 'Formula', 'Chemical formula'),
+            KD('pbc', 'PBC', 'Periodic boundary conditions'),
+            KD('user', 'Username'),
+            KD('calculator', 'Calculator', 'ASE-calculator name'),
+            KD('energy', 'Energy', 'Total energy', unit='eV'),
+            KD('natoms', 'Number of atoms'),
+            KD('fmax', 'Maximum force', unit='eV/Å'),
+            KD(
+                'smax',
+                'Maximum stress',
+                'Maximum stress on unit cell',
+                unit='eV/Å³',
+            ),
+            KD('charge', 'Charge', 'Net charge in unit cell', unit='|e|'),
+            KD('mass', 'Mass', 'Sum of atomic masses in unit cell', unit='au'),
+            KD('magmom', 'Magnetic moment', unit='μ_B'),
+            KD('unique_id', 'Unique ID', 'Random (unique) ID'),
+            KD('volume', 'Volume', 'Volume of unit cell', unit='Å³'),
+        ]
+    }
 
 
 def now():
@@ -88,48 +97,69 @@ def now():
     return (time() - T2000) / YEAR
 
 
-seconds = {'s': 1,
-           'm': 60,
-           'h': 3600,
-           'd': 86400,
-           'w': 604800,
-           'M': 2629800,
-           'y': YEAR}
-
-longwords = {'s': 'second',
-             'm': 'minute',
-             'h': 'hour',
-             'd': 'day',
-             'w': 'week',
-             'M': 'month',
-             'y': 'year'}
-
-ops = {'<': operator.lt,
-       '<=': operator.le,
-       '=': operator.eq,
-       '>=': operator.ge,
-       '>': operator.gt,
-       '!=': operator.ne}
+seconds = {
+    's': 1,
+    'm': 60,
+    'h': 3600,
+    'd': 86400,
+    'w': 604800,
+    'M': 2629800,
+    'y': YEAR,
+}
+
+longwords = {
+    's': 'second',
+    'm': 'minute',
+    'h': 'hour',
+    'd': 'day',
+    'w': 'week',
+    'M': 'month',
+    'y': 'year',
+}
+
+ops = {
+    '<': operator.lt,
+    '<=': operator.le,
+    '=': operator.eq,
+    '>=': operator.ge,
+    '>': operator.gt,
+    '!=': operator.ne,
+}
 
 invop = {'<': '>=', '<=': '>', '>=': '<', '>': '<=', '=': '!=', '!=': '='}
 
 word = re.compile('[_a-zA-Z][_0-9a-zA-Z]*$')
 
-reserved_keys = set(all_properties +
-                    all_changes +
-                    list(atomic_numbers) +
-                    ['id', 'unique_id', 'ctime', 'mtime', 'user',
-                     'fmax', 'smax',
-                     'momenta', 'constraints', 'natoms', 'formula', 'age',
-                     'calculator', 'calculator_parameters',
-                     'key_value_pairs', 'data'])
+reserved_keys = set(
+    all_properties
+    + all_changes
+    + list(atomic_numbers)
+    + [
+        'id',
+        'unique_id',
+        'ctime',
+        'mtime',
+        'user',
+        'fmax',
+        'smax',
+        'momenta',
+        'constraints',
+        'natoms',
+        'formula',
+        'age',
+        'calculator',
+        'calculator_parameters',
+        'key_value_pairs',
+        'data',
+    ]
+)
 
 numeric_keys = {'id', 'energy', 'magmom', 'charge', 'natoms'}
 
 
 def check(key_value_pairs):
     for key, value in key_value_pairs.items():
-        if key == "external_tables":
+        if key == 'external_tables':
             # Checks for external_tables are not
             # performed
             continue
@@ -145,19 +175,23 @@ def check(key_value_pairs):
                 'It is best not to use keys ({0}) that are also a '
                 'chemical formula.  If you do a "db.select({0!r})",'
                 'you will not find rows with your key.  Instead, you wil get '
-                'rows containing the atoms in the formula!'.format(key))
+                'rows containing the atoms in the formula!'.format(key)
+            )
         if not isinstance(value, (numbers.Real, str, np.bool_)):
             raise ValueError(f'Bad value for {key!r}: {value}')
         if isinstance(value, str):
             for t in [bool, int, float]:
                 if str_represents(value, t):
                     raise ValueError(
-                        'Value ' + value + ' is put in as string ' +
-                        'but can be interpreted as ' +
-                        f'{t.__name__}! Please convert ' +
-                        f'to {t.__name__} before ' +
-                        'writing to the database OR change ' +
-                        'to a different string.')
+                        'Value '
+                        + value
+                        + ' is put in as string '
+                        + 'but can be interpreted as '
+                        + f'{t.__name__}! Please convert '
+                        + f'to {t.__name__} before '
+                        + 'writing to the database OR change '
+                        + 'to a different string.'
+                    )
 
 
 def str_represents(value, t=int):
@@ -165,21 +199,30 @@ def str_represents(value, t=int):
     return isinstance(new_value, t)
 
 
-def connect(name, type='extract_from_name', create_indices=True,
-            use_lock_file=True, append=True, serial=False):
+def connect(
+    name,
+    type='extract_from_name',
+    create_indices=True,
+    use_lock_file=True,
+    append=True,
+    serial=False,
+    **db_kwargs,
+):
     """Create connection to database.
 
     name: str
         Filename or address of database.
     type: str
-        One of 'json', 'db', 'postgresql',
-        (JSON, SQLite, PostgreSQL).
+        One of 'json', 'db', 'postgresql', 'mysql', 'aselmdb'
+        (JSON, SQLite, PostgreSQL, MYSQL, ASELMDB).
         Default is 'extract_from_name', which will guess the type
         from the name.
     use_lock_file: bool
         You can turn this off if you know what you are doing ...
     append: bool
         Use append=False to start a new database.
+    db_kwargs: dict
+        Optional extra kwargs to pass on to the underlying db
     """
 
     if isinstance(name, PurePath):
@@ -190,8 +233,7 @@ def connect(name, type='extract_from_nam
             type = None
         elif not isinstance(name, str):
             type = 'json'
-        elif (name.startswith('postgresql://') or
-              name.startswith('postgres://')):
+        elif name.startswith('postgresql://') or name.startswith('postgres://'):
             type = 'postgresql'
         elif name.startswith('mysql://') or name.startswith('mariadb://'):
             type = 'mysql'
@@ -201,7 +243,7 @@ def connect(name, type='extract_from_nam
                 raise ValueError('No file extension or database type given')
 
     if type is None:
-        return Database()
+        return Database(**db_kwargs)
 
     if not append and world.rank == 0:
         if isinstance(name, str) and os.path.isfile(name):
@@ -212,23 +254,37 @@ def connect(name, type='extract_from_nam
 
     if type == 'json':
         from ase.db.jsondb import JSONDatabase
-        return JSONDatabase(name, use_lock_file=use_lock_file, serial=serial)
+
+        return JSONDatabase(
+            name, use_lock_file=use_lock_file, serial=serial, **db_kwargs
+        )
     if type == 'db':
         from ase.db.sqlite import SQLite3Database
-        return SQLite3Database(name, create_indices, use_lock_file,
-                               serial=serial)
+
+        return SQLite3Database(
+            name, create_indices, use_lock_file, serial=serial, **db_kwargs
+        )
     if type == 'postgresql':
-        from ase.db.postgresql import PostgreSQLDatabase
-        return PostgreSQLDatabase(name)
+        from ase_db_backends.postgresql import PostgreSQLDatabase
+
+        return PostgreSQLDatabase(name, **db_kwargs)
 
     if type == 'mysql':
-        from ase.db.mysql import MySQLDatabase
-        return MySQLDatabase(name)
+        from ase_db_backends.mysql import MySQLDatabase
+
+        return MySQLDatabase(name, **db_kwargs)
+
+    if type == 'aselmdb':
+        from ase_db_backends.aselmdb import LMDBDatabase
+
+        return LMDBDatabase(name, **db_kwargs)
+
     raise ValueError('Unknown database type: ' + type)
 
 
 def lock(method):
     """Decorator for using a lock-file."""
+
     @functools.wraps(method)
     def new_method(self, *args, **kwargs):
         if self.lock is None:
@@ -236,6 +292,7 @@ def lock(method):
         else:
             with self.lock:
                 return method(self, *args, **kwargs)
+
     return new_method
 
 
@@ -287,8 +344,9 @@ def parse_selection(selection, **kwargs)
                 except ValueError:
                     keys.append(expression)
                 else:
-                    comparisons.extend((symbol, '>', n - 1)
-                                       for symbol, n in count.items())
+                    comparisons.extend(
+                        (symbol, '>', n - 1) for symbol, n in count.items()
+                    )
             continue
         key, value = expression.split(op)
         comparisons.append((key, op, value))
@@ -307,15 +365,17 @@ def parse_selection(selection, **kwargs)
                 raise ValueError('Use fomula=...')
             f = Formula(value)
             count = f.count()
-            cmps.extend((atomic_numbers[symbol], '=', n)
-                        for symbol, n in count.items())
+            cmps.extend(
+                (atomic_numbers[symbol], '=', n) for symbol, n in count.items()
+            )
             key = 'natoms'
             value = len(f)
         elif key in atomic_numbers:
             key = atomic_numbers[key]
             value = int(value)
         elif isinstance(value, str):
-            value = convert_str_to_int_float_bool_or_str(value)
+            if key != 'unique_id':
+                value = convert_str_to_int_float_bool_or_str(value)
         if key in numeric_keys and not isinstance(value, (int, float)):
             msg = 'Wrong type for "{}{}{}" - must be a number'
             raise ValueError(msg.format(key, op, value))
@@ -327,8 +387,13 @@ def parse_selection(selection, **kwargs)
 class Database:
     """Base class for all databases."""
 
-    def __init__(self, filename=None, create_indices=True,
-                 use_lock_file=False, serial=False):
+    def __init__(
+        self,
+        filename: str = None,
+        create_indices: bool = True,
+        use_lock_file: bool = False,
+        serial: bool = False,
+    ):
         """Database object.
 
         serial: bool
@@ -340,14 +405,13 @@ class Database:
             filename = os.path.expanduser(filename)
         self.filename = filename
         self.create_indices = create_indices
+        self.lock = None
         if use_lock_file and isinstance(filename, str):
             self.lock = Lock(filename + '.lock', world=DummyMPI())
-        else:
-            self.lock = None
         self.serial = serial
 
         # Decription of columns and other stuff:
-        self._metadata: Dict[str, Any] = None
+        self._metadata = None
 
     @property
     def metadata(self) -> Dict[str, Any]:
@@ -403,9 +467,9 @@ class Database:
         anything and return None.
         """
 
-        for _ in self._select([],
-                              [(key, '=', value)
-                               for key, value in key_value_pairs.items()]):
+        for _ in self._select(
+            [], [(key, '=', value) for key, value in key_value_pairs.items()]
+        ):
             return None
 
         atoms = Atoms()
@@ -435,8 +499,9 @@ class Database:
     def __delitem__(self, id):
         self.delete([id])
 
-    def get_atoms(self, selection=None,
-                  add_additional_information=False, **kwargs):
+    def get_atoms(
+        self, selection=None, add_additional_information=False, **kwargs
+    ):
         """Get Atoms object.
 
         selection: int, str or list
@@ -467,9 +532,19 @@ class Database:
         return rows[0]
 
     @parallel_generator
-    def select(self, selection=None, filter=None, explain=False,
-               verbosity=1, limit=None, offset=0, sort=None,
-               include_data=True, columns='all', **kwargs):
+    def select(
+        self,
+        selection=None,
+        filter=None,
+        explain=False,
+        verbosity=1,
+        limit=None,
+        offset=0,
+        sort=None,
+        include_data=True,
+        columns='all',
+        **kwargs,
+    ):
         """Select rows.
 
         Return AtomsRow iterator with results.  Selection is done
@@ -516,11 +591,17 @@ class Database:
                 sort += 'name'
 
         keys, cmps = parse_selection(selection, **kwargs)
-        for row in self._select(keys, cmps, explain=explain,
-                                verbosity=verbosity,
-                                limit=limit, offset=offset, sort=sort,
-                                include_data=include_data,
-                                columns=columns):
+        for row in self._select(
+            keys,
+            cmps,
+            explain=explain,
+            verbosity=verbosity,
+            limit=limit,
+            offset=offset,
+            sort=sort,
+            include_data=include_data,
+            columns=columns,
+        ):
             if filter is None or filter(row):
                 yield row
 
@@ -540,8 +621,9 @@ class Database:
 
     @parallel_function
     @lock
-    def update(self, id, atoms=None, delete_keys=[], data=None,
-               **add_key_value_pairs):
+    def update(
+        self, id, atoms=None, delete_keys=[], data=None, **add_key_value_pairs
+    ):
         """Update and/or delete key-value pairs of row(s).
 
         id: int
@@ -560,11 +642,13 @@ class Database:
 
         if not isinstance(id, numbers.Integral):
             if isinstance(id, list):
-                err = ('First argument must be an int and not a list.\n'
-                       'Do something like this instead:\n\n'
-                       'with db:\n'
-                       '    for id in ids:\n'
-                       '        db.update(id, ...)')
+                err = (
+                    'First argument must be an int and not a list.\n'
+                    'Do something like this instead:\n\n'
+                    'with db:\n'
+                    '    for id in ids:\n'
+                    '        db.update(id, ...)'
+                )
                 raise ValueError(err)
             raise TypeError('id must be an int')
 
@@ -667,15 +751,14 @@ def o2b(obj: Any, parts: List[bytes]):
     if isinstance(obj, (list, tuple)):
         return [o2b(value, parts) for value in obj]
     if isinstance(obj, np.ndarray):
-        assert obj.dtype != object, \
+        assert obj.dtype != object, (
             'Cannot convert ndarray of type "object" to bytes.'
+        )
         offset = sum(len(part) for part in parts)
         if not np.little_endian:
             obj = obj.byteswap()
         parts.append(obj.tobytes())
-        return {'__ndarray__': [obj.shape,
-                                obj.dtype.name,
-                                offset]}
+        return {'__ndarray__': [obj.shape, obj.dtype.name, offset]}
     if isinstance(obj, complex):
         return {'__complex__': [obj.real, obj.imag]}
     objtype = obj.ase_objtype
@@ -683,8 +766,9 @@ def o2b(obj: Any, parts: List[bytes]):
         dct = o2b(obj.todict(), parts)
         dct['__ase_objtype__'] = objtype
         return dct
-    raise ValueError('Objects of type {type} not allowed'
-                     .format(type=type(obj)))
+    raise ValueError(
+        'Objects of type {type} not allowed'.format(type=type(obj))
+    )
 
 
 def b2o(obj: Any, b: bytes) -> Any:
@@ -705,7 +789,7 @@ def b2o(obj: Any, b: bytes) -> Any:
         shape, name, offset = x
         dtype = np.dtype(name)
         size = dtype.itemsize * np.prod(shape).astype(int)
-        a = np.frombuffer(b[offset:offset + size], dtype)
+        a = np.frombuffer(b[offset : offset + size], dtype)
         a.shape = shape
         if not np.little_endian:
             a = a.byteswap()
diff -pruN 3.24.0-1/ase/db/jsondb.py 3.26.0-1/ase/db/jsondb.py
--- 3.24.0-1/ase/db/jsondb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/jsondb.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import sys
 from contextlib import ExitStack
diff -pruN 3.24.0-1/ase/db/mysql.py 3.26.0-1/ase/db/mysql.py
--- 3.24.0-1/ase/db/mysql.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/mysql.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,280 +0,0 @@
-import json
-import sys
-from copy import deepcopy
-
-import numpy as np
-from pymysql import connect
-from pymysql.err import ProgrammingError
-
-import ase.io.jsonio
-from ase.db.postgresql import insert_nan_and_inf, remove_nan_and_inf
-from ase.db.sqlite import VERSION, SQLite3Database, init_statements
-
-
-class Connection:
-    """
-    Wrapper for the MySQL connection
-
-    Arguments
-    =========
-    host: str
-        Hostname. For a local database this is localhost.
-    user: str
-        Username.
-    passwd: str
-        Password
-    db_name: str
-        Name of the database
-    port: int
-        Port
-    binary_prefix: bool
-        MySQL checks if an argument can be interpreted as a UTF-8 string. This
-        check fails for binary values. Binary values need to have _binary
-        prefix in MySQL. By setting this to True, the prefix is automatically
-        added for binary values.
-    """
-
-    def __init__(self, host=None, user=None, passwd=None, port=3306,
-                 db_name=None, binary_prefix=False):
-        self.con = connect(host=host, user=user, passwd=passwd, db=db_name,
-                           binary_prefix=binary_prefix, port=port)
-
-    def cursor(self):
-        return MySQLCursor(self.con.cursor())
-
-    def commit(self):
-        self.con.commit()
-
-    def close(self):
-        self.con.close()
-
-    def rollback(self):
-        self.con.rollback()
-
-
-class MySQLCursor:
-    """
-    Wrapper for the MySQL cursor. The most important task performed by this
-    class is to translate SQLite queries to MySQL. Translation is needed
-    because ASE DB uses some field names that are reserved words in MySQL.
-    Thus, these has to mapped onto other field names.
-    """
-    sql_replace = [
-        (' key TEXT', ' attribute_key TEXT'),
-        ('(key TEXT', '(attribute_key TEXT'),
-        ('SELECT key FROM', 'SELECT attribute_key FROM'),
-        ('SELECT DISTINCT key FROM keys',
-         'SELECT DISTINCT attribute_key FROM attribute_keys'),
-        ('?', '%s'),
-        (' keys ', ' attribute_keys '),
-        (' key=', ' attribute_key='),
-        ('table.key', 'table.attribute_key'),
-        (' IF NOT EXISTS', '')]
-
-    def __init__(self, cur):
-        self.cur = cur
-
-    def execute(self, sql, params=None):
-
-        # Replace external table key -> attribute_key
-        for substibution in self.sql_replace:
-            sql = sql.replace(substibution[0], substibution[1])
-
-        if params is None:
-            params = ()
-
-        self.cur.execute(sql, params)
-
-    def fetchone(self):
-        return self.cur.fetchone()
-
-    def fetchall(self):
-        return self.cur.fetchall()
-
-    def _replace_nan_inf_kvp(self, values):
-        for item in values:
-            if not np.isfinite(item[1]):
-                item[1] = sys.float_info.max / 2
-        return values
-
-    def executemany(self, sql, values):
-        if 'number_key_values' in sql:
-            values = self._replace_nan_inf_kvp(values)
-
-        for substibution in self.sql_replace:
-            sql = sql.replace(substibution[0], substibution[1])
-        self.cur.executemany(sql, values)
-
-
-class MySQLDatabase(SQLite3Database):
-    """
-    ASE interface to a MySQL database (via pymysql package).
-
-    Arguments
-    ==========
-    url: str
-        URL to the database. It should have the form
-        mysql://username:password@host:port/database_name.
-        Example URL with the following credentials
-            username: john
-            password: johnspasswd
-            host: localhost (i.e. server is running locally)
-            database: johns_calculations
-            port: 3306
-        mysql://john:johnspasswd@localhost:3306/johns_calculations
-    create_indices: bool
-        Carried over from parent class. Currently indices are not
-        created for MySQL, as TEXT fields cannot be hashed by MySQL.
-    use_lock_file: bool
-        See SQLite
-    serial: bool
-        See SQLite
-    """
-    type = 'mysql'
-    default = 'DEFAULT'
-
-    def __init__(self, url=None, create_indices=True,
-                 use_lock_file=False, serial=False):
-        super().__init__(
-            url, create_indices, use_lock_file, serial)
-
-        self.host = None
-        self.username = None
-        self.passwd = None
-        self.db_name = None
-        self.port = 3306
-        self._parse_url(url)
-
-    def _parse_url(self, url):
-        """
-        Parse the URL
-        """
-        url = url.replace('mysql://', '')
-        url = url.replace('mariadb://', '')
-
-        splitted = url.split(':', 1)
-        self.username = splitted[0]
-
-        splitted = splitted[1].split('@')
-        self.passwd = splitted[0]
-
-        splitted = splitted[1].split('/')
-        host_and_port = splitted[0].split(':')
-        self.host = host_and_port[0]
-        self.port = int(host_and_port[1])
-        self.db_name = splitted[1]
-
-    def _connect(self):
-        return Connection(host=self.host, user=self.username,
-                          passwd=self.passwd, db_name=self.db_name,
-                          port=self.port, binary_prefix=True)
-
-    def _initialize(self, con):
-        if self.initialized:
-            return
-
-        cur = con.cursor()
-
-        information_exists = True
-        self._metadata = {}
-        try:
-            cur.execute("SELECT 1 FROM information")
-        except ProgrammingError:
-            information_exists = False
-
-        if not information_exists:
-            # We need to initialize the DB
-            # MySQL require that id is explicitly set as primary key
-            # in the systems table
-            init_statements_cpy = deepcopy(init_statements)
-            init_statements_cpy[0] = init_statements_cpy[0][:-1] + \
-                ', PRIMARY KEY(id))'
-
-            statements = schema_update(init_statements_cpy)
-            for statement in statements:
-                cur.execute(statement)
-            con.commit()
-            self.version = VERSION
-        else:
-            cur.execute('select * from information')
-
-            for name, value in cur.fetchall():
-                if name == 'version':
-                    self.version = int(value)
-                elif name == 'metadata':
-                    self._metadata = json.loads(value)
-
-        self.initialized = True
-
-    def blob(self, array):
-        if array is None:
-            return None
-        return super().blob(array).tobytes()
-
-    def get_offset_string(self, offset, limit=None):
-        sql = ''
-        if not limit:
-            # mysql does not allow for setting limit to -1 so
-            # instead we set a large number
-            sql += '\nLIMIT 10000000000'
-        sql += f'\nOFFSET {offset}'
-        return sql
-
-    def get_last_id(self, cur):
-        cur.execute('select max(id) as ID from systems')
-        last_id = cur.fetchone()[0]
-        return last_id
-
-    def create_select_statement(self, keys, cmps,
-                                sort=None, order=None, sort_table=None,
-                                what='systems.*'):
-        sql, value = super().create_select_statement(
-            keys, cmps, sort, order, sort_table, what)
-
-        for subst in MySQLCursor.sql_replace:
-            sql = sql.replace(subst[0], subst[1])
-        return sql, value
-
-    def encode(self, obj, binary=False):
-        return ase.io.jsonio.encode(remove_nan_and_inf(obj))
-
-    def decode(self, obj, lazy=False):
-        return insert_nan_and_inf(ase.io.jsonio.decode(obj))
-
-
-def schema_update(statements):
-    for i, statement in enumerate(statements):
-        for a, b in [('REAL', 'DOUBLE'),
-                     ('INTEGER PRIMARY KEY AUTOINCREMENT',
-                      'INT NOT NULL AUTO_INCREMENT')]:
-            statements[i] = statement.replace(a, b)
-
-    # MySQL does not support UNIQUE constraint on TEXT
-    # need to use VARCHAR. The unique_id is generated with
-    # randint(16**31, 16**32-1) so it will contain 32
-    # hex-characters
-    statements[0] = statements[0].replace('TEXT UNIQUE', 'VARCHAR(32) UNIQUE')
-
-    # keys is a reserved word in MySQL redefine this table name to
-    # attribute_keys
-    statements[2] = statements[2].replace('keys', 'attribute_keys')
-
-    txt2jsonb = ['calculator_parameters', 'key_value_pairs']
-
-    for column in txt2jsonb:
-        statements[0] = statements[0].replace(
-            f'{column} TEXT,',
-            f'{column} JSON,')
-
-    statements[0] = statements[0].replace('data BLOB,', 'data JSON,')
-
-    tab_with_key_field = ['attribute_keys', 'number_key_values',
-                          'text_key_values']
-
-    # key is a reserved word in MySQL redefine this to attribute_key
-    for i, statement in enumerate(statements):
-        for tab in tab_with_key_field:
-            if tab in statement:
-                statements[i] = statement.replace(
-                    'key TEXT', 'attribute_key TEXT')
-    return statements
diff -pruN 3.24.0-1/ase/db/postgresql.py 3.26.0-1/ase/db/postgresql.py
--- 3.24.0-1/ase/db/postgresql.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/postgresql.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,215 +0,0 @@
-import json
-
-import numpy as np
-from psycopg2 import connect
-from psycopg2.extras import execute_values
-
-from ase.db.sqlite import (
-    VERSION,
-    SQLite3Database,
-    index_statements,
-    init_statements,
-)
-from ase.io.jsonio import create_ase_object, create_ndarray
-from ase.io.jsonio import encode as ase_encode
-
-jsonb_indices = [
-    'CREATE INDEX idxkeys ON systems USING GIN (key_value_pairs);',
-    'CREATE INDEX idxcalc ON systems USING GIN (calculator_parameters);']
-
-
-def remove_nan_and_inf(obj):
-    if isinstance(obj, float) and not np.isfinite(obj):
-        return {'__special_number__': str(obj)}
-    if isinstance(obj, list):
-        return [remove_nan_and_inf(x) for x in obj]
-    if isinstance(obj, dict):
-        return {key: remove_nan_and_inf(value) for key, value in obj.items()}
-    if isinstance(obj, np.ndarray) and not np.isfinite(obj).all():
-        return remove_nan_and_inf(obj.tolist())
-    return obj
-
-
-def insert_nan_and_inf(obj):
-    if isinstance(obj, dict) and '__special_number__' in obj:
-        return float(obj['__special_number__'])
-    if isinstance(obj, list):
-        return [insert_nan_and_inf(x) for x in obj]
-    if isinstance(obj, dict):
-        return {key: insert_nan_and_inf(value) for key, value in obj.items()}
-    return obj
-
-
-class Connection:
-    def __init__(self, con):
-        self.con = con
-
-    def cursor(self):
-        return Cursor(self.con.cursor())
-
-    def commit(self):
-        self.con.commit()
-
-    def close(self):
-        self.con.close()
-
-
-class Cursor:
-    def __init__(self, cur):
-        self.cur = cur
-
-    def fetchone(self):
-        return self.cur.fetchone()
-
-    def fetchall(self):
-        return self.cur.fetchall()
-
-    def execute(self, statement, *args):
-        self.cur.execute(statement.replace('?', '%s'), *args)
-
-    def executemany(self, statement, *args):
-        if len(args[0]) > 0:
-            N = len(args[0][0])
-        else:
-            return
-        if 'INSERT INTO systems' in statement:
-            q = 'DEFAULT' + ', ' + ', '.join('?' * N)  # DEFAULT for id
-        else:
-            q = ', '.join('?' * N)
-        statement = statement.replace(f'({q})', '%s')
-        q = '({})'.format(q.replace('?', '%s'))
-
-        execute_values(self.cur, statement.replace('?', '%s'),
-                       argslist=args[0], template=q, page_size=len(args[0]))
-
-
-def insert_ase_and_ndarray_objects(obj):
-    if isinstance(obj, dict):
-        objtype = obj.pop('__ase_objtype__', None)
-        if objtype is not None:
-            return create_ase_object(objtype,
-                                     insert_ase_and_ndarray_objects(obj))
-        data = obj.get('__ndarray__')
-        if data is not None:
-            return create_ndarray(*data)
-        return {key: insert_ase_and_ndarray_objects(value)
-                for key, value in obj.items()}
-    if isinstance(obj, list):
-        return [insert_ase_and_ndarray_objects(value) for value in obj]
-    return obj
-
-
-class PostgreSQLDatabase(SQLite3Database):
-    type = 'postgresql'
-    default = 'DEFAULT'
-
-    def encode(self, obj, binary=False):
-        return ase_encode(remove_nan_and_inf(obj))
-
-    def decode(self, obj, lazy=False):
-        return insert_ase_and_ndarray_objects(insert_nan_and_inf(obj))
-
-    def blob(self, array):
-        """Convert array to blob/buffer object."""
-
-        if array is None:
-            return None
-        if len(array) == 0:
-            array = np.zeros(0)
-        if array.dtype == np.int64:
-            array = array.astype(np.int32)
-        return array.tolist()
-
-    def deblob(self, buf, dtype=float, shape=None):
-        """Convert blob/buffer object to ndarray of correct dtype and shape.
-
-        (without creating an extra view)."""
-        if buf is None:
-            return None
-        return np.array(buf, dtype=dtype)
-
-    def _connect(self):
-        return Connection(connect(self.filename))
-
-    def _initialize(self, con):
-        if self.initialized:
-            return
-
-        self._metadata = {}
-
-        cur = con.cursor()
-        cur.execute("show search_path;")
-        schema = cur.fetchone()[0].split(', ')
-        if schema[0] == '"$user"':
-            schema = schema[1]
-        else:
-            schema = schema[0]
-
-        cur.execute("""
-        SELECT EXISTS(select * from information_schema.tables where
-        table_name='information' and table_schema='{}');
-        """.format(schema))
-
-        if not cur.fetchone()[0]:  # information schema doesn't exist.
-            # Initialize database:
-            sql = ';\n'.join(init_statements)
-            sql = schema_update(sql)
-            cur.execute(sql)
-            if self.create_indices:
-                cur.execute(';\n'.join(index_statements))
-                cur.execute(';\n'.join(jsonb_indices))
-            con.commit()
-            self.version = VERSION
-        else:
-            cur.execute('select * from information;')
-            for name, value in cur.fetchall():
-                if name == 'version':
-                    self.version = int(value)
-                elif name == 'metadata':
-                    self._metadata = json.loads(value)
-
-        assert 5 < self.version <= VERSION
-
-        self.initialized = True
-
-    def get_offset_string(self, offset, limit=None):
-        # postgresql allows you to set offset without setting limit;
-        # very practical
-        return f'\nOFFSET {offset}'
-
-    def get_last_id(self, cur):
-        cur.execute('SELECT last_value FROM systems_id_seq')
-        id = cur.fetchone()[0]
-        return int(id)
-
-
-def schema_update(sql):
-    for a, b in [('REAL', 'DOUBLE PRECISION'),
-                 ('INTEGER PRIMARY KEY AUTOINCREMENT',
-                  'SERIAL PRIMARY KEY')]:
-        sql = sql.replace(a, b)
-
-    arrays_1D = ['numbers', 'initial_magmoms', 'initial_charges', 'masses',
-                 'tags', 'momenta', 'stress', 'dipole', 'magmoms', 'charges']
-
-    arrays_2D = ['positions', 'cell', 'forces']
-
-    txt2jsonb = ['calculator_parameters', 'key_value_pairs']
-
-    for column in arrays_1D:
-        if column in ['numbers', 'tags']:
-            dtype = 'INTEGER'
-        else:
-            dtype = 'DOUBLE PRECISION'
-        sql = sql.replace(f'{column} BLOB,',
-                          f'{column} {dtype}[],')
-    for column in arrays_2D:
-        sql = sql.replace(f'{column} BLOB,',
-                          f'{column} DOUBLE PRECISION[][],')
-    for column in txt2jsonb:
-        sql = sql.replace(f'{column} TEXT,',
-                          f'{column} JSONB,')
-
-    sql = sql.replace('data BLOB,', 'data JSONB,')
-
-    return sql
diff -pruN 3.24.0-1/ase/db/project.py 3.26.0-1/ase/db/project.py
--- 3.24.0-1/ase/db/project.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/project.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from pathlib import Path
 
 from ase.db.core import KeyDescription
diff -pruN 3.24.0-1/ase/db/row.py 3.26.0-1/ase/db/row.py
--- 3.24.0-1/ase/db/row.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/row.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,6 @@
-from random import randint
+# fmt: off
+
+import uuid
 from typing import Any, Dict
 
 import numpy as np
@@ -35,7 +37,7 @@ def atoms2dict(atoms):
     dct = {
         'numbers': atoms.numbers,
         'positions': atoms.positions,
-        'unique_id': '%x' % randint(16**31, 16**32 - 1)}
+        'unique_id': uuid.uuid4().hex}
     if atoms.pbc.any():
         dct['pbc'] = atoms.pbc
     if atoms.cell.any():
diff -pruN 3.24.0-1/ase/db/sqlite.py 3.26.0-1/ase/db/sqlite.py
--- 3.24.0-1/ase/db/sqlite.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/sqlite.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """SQLite3 backend.
 
 Versions:
diff -pruN 3.24.0-1/ase/db/table.py 3.26.0-1/ase/db/table.py
--- 3.24.0-1/ase/db/table.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/table.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import List, Optional
 
 import numpy as np
@@ -58,11 +60,17 @@ def cutlist(lst, length):
 
 
 class Table:
-    def __init__(self, connection, unique_key='id', verbosity=1, cut=35):
+    def __init__(
+        self,
+        connection,
+        unique_key: str = 'id',
+        verbosity: int = 1,
+        cut: int = 35,
+    ):
         self.connection = connection
         self.verbosity = verbosity
         self.cut = cut
-        self.rows = []
+        self.rows: list[Row] = []
         self.columns = None
         self.id = None
         self.right = None
diff -pruN 3.24.0-1/ase/db/templates/js.html 3.26.0-1/ase/db/templates/js.html
--- 3.24.0-1/ase/db/templates/js.html	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/templates/js.html	2025-08-12 11:26:23.000000000 +0000
@@ -1,9 +1,7 @@
-
-<!-- jQuery -->
-<script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
-
-<!-- jQuery UI -->
-<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU=" crossorigin="anonymous"></script>
-
-<!-- Latest compiled and minified JavaScript -->
-<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
\ No newline at end of file
+
+<!-- jQuery -->
+<script src="https://code.jquery.com/jquery-3.1.1.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
+
+<!-- jQuery UI -->
+<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.min.js" integrity="sha256-VazP97ZCwtekAsvgPBSUwPFKdrwD3unUfSGVYrahUqU=" crossorigin="anonymous"></script>
+
diff -pruN 3.24.0-1/ase/db/templates/layout.html 3.26.0-1/ase/db/templates/layout.html
--- 3.24.0-1/ase/db/templates/layout.html	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/templates/layout.html	2025-08-12 11:26:23.000000000 +0000
@@ -3,8 +3,8 @@
   <head>
     <meta name="viewport" content="width-device-width, initial-scale=1.0">
 
-{% include 'ase/db/templates/js.html' %}
 {% include 'ase/db/templates/style.html' %}
+{% include 'ase/db/templates/js.html' %}
 {% block head %}
 {% endblock %}
 
@@ -14,9 +14,9 @@
 
   <body {% block BodySettings %}{% endblock %}>
 
-    <div class="container" style="padding: 0px;">
+    <div class="container-fluid" style="padding: 5px;">
       <div class="row">
-        <nav class="navbar navbar-default">
+        <nav class="navbar navbar-default px-1">
           <div class="container-fluid">
             <ul class="nav navbar-nav navbar-right">
               {% block navbar %} {% endblock %}
diff -pruN 3.24.0-1/ase/db/templates/row.html 3.26.0-1/ase/db/templates/row.html
--- 3.24.0-1/ase/db/templates/row.html	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/templates/row.html	2025-08-12 11:26:23.000000000 +0000
@@ -1,37 +1,35 @@
 {% extends 'ase/db/templates/layout.html' %}
 
 {% macro atoms() %}
-<div class="row">
-  <center>
-    <div id="appdiv"></div>
-  </center>
-  <div class="row">
-    <div class="col-md-1"></div>
-    <div class="col-md-4">
-      <a class="btn" href="/gui/{{ row.id }}">Open ASE's GUI</a>
-      <div class="btn-group pull-right">
-        <button type="button" class="btn btn-link dropdown-toggle btn-sm"
-                data-toggle="dropdown">
-          Unit cell <span class="caret"></span>
-        </button>
-        <ul class="dropdown-menu">
-          <li><a onclick="repeatCell(1, 1, 1);">1x1x1</a></li>
-          <li><a onclick="repeatCell({{ n1 }}, {{ n2 }}, {{ n3 }});">
-              {{ n1 }}x{{ n2 }}x{{ n3 }}</a></li>
-        </ul>
-      </div>
-      <div class="btn-group pull-right">
-        <button type="button" class="btn btn-link dropdown-toggle btn-sm"
-                data-toggle="dropdown">
-          Download <span class="caret"></span>
-        </button>
-        <ul class="dropdown-menu">
-          <li><a href="/atoms/{{ project.name }}/{{ row.id }}/xyz">xyz</a></li>
-          <li><a href="/atoms/{{ project.name }}/{{ row.id }}/json">json</a></li>
-        </ul>
-      </div>
+<div id="appdiv"></div>
+<div class="row align-items-center">
+  <div class="col-6">
+    <a class="btn btn-primary btn-sm" href="/gui/{{ row.id }}">Open ASE's GUI</a>
+  </div>
+  <div class="col-3 px-1">
+    <div class="btn-group pull-right">
+      <button type="button" class="btn btn-primary dropdown-toggle btn-sm"
+              data-bs-toggle="dropdown">
+        Unit cell <span class="caret"></span>
+      </button>
+      <ul class="dropdown-menu">
+        <li><a onclick="repeatCell(1, 1, 1);">1x1x1</a></li>
+        <li><a onclick="repeatCell({{ n1 }}, {{ n2 }}, {{ n3 }});">
+            {{ n1 }}x{{ n2 }}x{{ n3 }}</a></li>
+      </ul>
+    </div>
+  </div>
+  <div class="col-3">
+    <div class="btn-group pull-right">
+      <button type="button" class="btn btn-primary dropdown-toggle btn-sm"
+              data-bs-toggle="dropdown">
+        Download <span class="caret"></span>
+      </button>
+      <ul class="dropdown-menu">
+        <li><a href="/atoms/{{ project.name }}/{{ row.id }}/xyz">xyz</a></li>
+        <li><a href="/atoms/{{ project.name }}/{{ row.id }}/json">json</a></li>
+      </ul>
     </div>
-  <div class="col-md-1"></div>
   </div>
 </div>
 {% endmacro %}
@@ -102,62 +100,39 @@ jmol_isReady = function(applet)
 
 {% block content %}
 
-<div class="container">
+<div class="container-fluid">
   <h1>{{ dct.formula|safe }}</h1>
 
-  <div class="panel-group">
-
-    <div class="panel panel-default">
-      <div class="panel-heading">
-        <h4 class="panel-title">
-          <a class="btn-block" data-toggle="collapse" href="#collapse0">
-          Structure </a>
-        </h4>
-      </div>
-
-      <div id="collapse0" class="panel-collapse collapse in">
-        <div class="panel-body">
-          <div class="col-md-6">
-            <div class="row">
-              {{ atoms() }}
-            </div> <!--END ROW-->
-            <div class="row">
-              {{ cell() }}
-            </div> <!--END ROW-->
-          </div> <!--END col-md-6-->
-        </div> <!--END PANEL BODY-->
-      </div> <!--END COLLAPSE-->
-
-      <div class="panel-heading">
-        <h4 class="panel-title">
-          <a class="btn-block" data-toggle="collapse" href="#collapse1">
-          Keys and values</a>
-        </h4>
-      </div>
-
-      <div id="collapse1" class="panel-collapse collapse in">
-        <div class="panel-body">
-          <table class="table table-striped">
-            <thead>
-              <tr>
-                <th>Key</th>
-                <th>Description</th>
-                <th>Value</th>
-              </tr>
-            </thead>
-            <tbody>
-            {% for key, desc, val in dct.table %}
-              <tr>
-                <td> {{ key }} </td>
-                <td> {{ desc }} </td>
-                <td> {{ val|safe }} </td>
-              </tr>
-            {% endfor %}
-            </tbody>
-          </table>
-        </div> <!--END PANEL BODY-->
-      </div> <!--END COLLAPSE-->
-    </div> <!--END PANEL-->
-  </div> <!--END PANEL GROUP-->
+  <h4 class="card-title">Structure</h4>
+  <div class="card">
+    <div class="col-md-6 p-2">
+      {{ atoms() }}
+      <div class="col">{{ cell() }}</div>
+    </div> <!--END col-md-6-->
+  </div> <!--END CARD-->
+
+  <div class="col-9">
+    <h4 class="card-title mt-2">Keys and values</h4>
+    <div class="card">
+      <table class="table table-striped">
+        <thead>
+          <tr>
+            <th>Key</th>
+            <th>Description</th>
+            <th>Value</th>
+          </tr>
+        </thead>
+        <tbody>
+        {% for key, desc, val in dct.table %}
+          <tr>
+            <td> {{ key }} </td>
+            <td> {{ desc }} </td>
+            <td> {{ val|safe }} </td>
+          </tr>
+        {% endfor %}
+        </tbody>
+      </table>
+    </div> <!--END Col-9-->
+  </div>
 </div>
 {% endblock content %}
diff -pruN 3.24.0-1/ase/db/templates/search.html 3.26.0-1/ase/db/templates/search.html
--- 3.24.0-1/ase/db/templates/search.html	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/templates/search.html	2025-08-12 11:26:23.000000000 +0000
@@ -24,24 +24,24 @@ document.addEventListener('DOMContentLoa
   </div>
   {% endblock %}
 
-  <div class="well mt-2">
+  <div class="card text-bg-light shadow-sm mt-2">
     <form id="mainFormID" class="navbar-form navbar-default mt-2"
           role="search"
           action="javascript:update_table({{ session_id }}, 'query', '0')">
-      <div class="form-group" style="margin-top:5px;">
+      <div class="form-group d-flex align-items-center col-auto" style="margin-top:5px;">
         <input type="text" name="query" id="formula-result"
-               class="form-control mt-2 ase-input"
+               class="form-control mx-2 ase-input"
                value="{{ q }}"
                placeholder="Search formula e.g. MoS2" size="60">
-        <button type="submit" class="btn btn-default">
-          <i class="fa fa-search fa-1x" aria-hidden="true"></i>
+        <button type="submit" class="btn btn-secondary mx-2">
+          Search
         </button>
       </div><br/>
       {% block search %}
       {% endblock search %}
-      <div class="form-group" style="margin-bottom:0px;">
-        <small class="form-text text-muted">
-          <a href="https://wiki.fysik.dtu.dk/ase/ase/db/db.html#querying"
+      <div class="form-group col-auto mx-2 col-form-label" style="margin-bottom:10px;">
+        <small class="form-text">
+          <a href="https://ase-lib.org/ase/db/db.html#querying"
              target="_blank">
             Help with constructing advanced search queries ...
           </a>
@@ -60,8 +60,8 @@ document.addEventListener('DOMContentLoa
         {% endif %}
       {% endwith %}
 
-      <small lcass="form-text text-muted">
-      <a data-toggle="collapse" href="#collapse1">Toggle list of keys ...</a>
+      <small class="form-text mx-2 mb-2">
+      <a data-bs-toggle="collapse" role="button" href="#collapse1" aria-controls="collapse1">Toggle list of keys ...</a>
       </small><br/>
       <div id="collapse1" class="collapse">
         <table class="table table-striped">
diff -pruN 3.24.0-1/ase/db/templates/style.html 3.26.0-1/ase/db/templates/style.html
--- 3.24.0-1/ase/db/templates/style.html	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/templates/style.html	2025-08-12 11:26:23.000000000 +0000
@@ -1,15 +1,17 @@
-
-<!-- Latest compiled and minified CSS -->
-<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
-
-<!-- Optional theme -->
-<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
-
-<!-- jQuery CSS -->
-<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.css">
-
-<!-- FontAwesome -->
-<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
-
-<!-- Custom Style -->
+
+<!-- Latest compiled and minified CSS -->
+<link
+href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
+rel="stylesheet"
+integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
+crossorigin="anonymous" />
+
+<script
+defered
+src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
+integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
+crossorigin="anonymous">
+</script>
+
+<!-- Custom Style -->
 <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
\ No newline at end of file
diff -pruN 3.24.0-1/ase/db/templates/table.html 3.26.0-1/ase/db/templates/table.html
--- 3.24.0-1/ase/db/templates/table.html	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/templates/table.html	2025-08-12 11:26:23.000000000 +0000
@@ -3,9 +3,8 @@
     <div class="panel-heading">
 
     <!-- Database Table -->
-
-      <div class="row">
-        <div class="col-xs-6">
+      <div class="row mt-2">
+        <div class="col">
           <b>
           Displaying rows {{ session.row1 }}-{{ session.row2 }} out of {{ session.nrows }}
           (total # or rows: {{ session.nrows_total }})
@@ -15,38 +14,40 @@
           {% endif %}
         </div>
 
-        <div class="col-xs-6">
-          <div class="btn-group pull-right">
+        <div class="col">
+          <div class="btn-toolbar justify-content-end bg-white">
+            <div class="dropdown">
             <button type="button"
-                    class="btn btn-default dropdown-toggle btn-sm"
-                    data-toggle="dropdown" aria-haspopup="true"
+                    class="btn btn-secondary btn-sm dropdown-toggle"
+                    data-bs-toggle="dropdown"
                     aria-expanded="false">
-            Add Column <span class="caret"></span>
+            Add Column
             </button>
             <ul class="dropdown-menu">
             {% for key, value in project.key_descriptions|dictsort(false, "value") if key in table.addcolumns %}
-              <li><a href="javascript:update_table({{ session.id }},
+              <li><a class="dropdown-item" href="javascript:update_table({{ session.id }},
                                                    'toggle',
                                                    '{{ key }}')">
               {{ value.longdesc }} ({{key}}) </a></li>
             {% endfor %}
             </ul>
-          </div>
+            </div>
 
-          <div class="btn-group pull-right">
+            <div class="dropdown">
             <button type="button"
-                    class="btn btn-default dropdown-toggle btn-sm"
-                    data-toggle="dropdown" aria-haspopup="true"
+                    class="btn btn-secondary btn-sm dropdown-toggle"
+                    data-bs-toggle="dropdown"
                     aria-expanded="false">
-            Rows: {{ session.limit }} <span class="caret"></span>
+            Rows: {{ session.limit }}
             </button>
-            <ul class="dropdown-menu">
+            <ul class="dropdown-menu" id="dropinfo" role="menu">
             {% for n in [10, 25, 50, 100, 200] %}
-              <li><a href="javascript:update_table({{ session.id }},
+              <li><a class="dropdown-item" href="javascript:update_table({{ session.id }},
                                                    'limit',
                                                    {{ n }})">{{ n }}</a></li>
             {% endfor %}
             </ul>
+            </div>
           </div>
         </div>
       </div>
@@ -56,7 +57,8 @@
     </div>
 
     <!-- Table -->
-    <table id="rows" class="table table-striped">
+    <div class="table-responsive mt-2">
+    <table id="rows" class="table table-hover table-striped">
 
     <tr>
     {%- for column in table.columns %}
@@ -70,7 +72,7 @@
       {% endif -%}
 
       {% if column == 'formula' %}
-        <span data-toggle="tooltip" title="key: formul">Formula</span>
+        <span data-toggle="tooltip" title="key: formula">Formula</span>
       {% else %}
         <a href="javascript:update_table({{ session.id }}, 'sort', '{{ column }}')"
            data-toggle="tooltip" title="key: {{column}}{{unit}}">
@@ -97,6 +99,7 @@
       </tr>
     {% endfor %}
     </table>
+    </div>
   </div>
 </div>
 
diff -pruN 3.24.0-1/ase/db/web.py 3.26.0-1/ase/db/web.py
--- 3.24.0-1/ase/db/web.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/db/web.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Helper functions for Flask WSGI-app."""
 from typing import Dict, List, Optional, Tuple
 
diff -pruN 3.24.0-1/ase/dependencies.py 3.26.0-1/ase/dependencies.py
--- 3.24.0-1/ase/dependencies.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dependencies.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,5 @@
-import importlib
+# fmt: off
+
 from typing import List, Tuple
 
 from ase.utils import (
@@ -11,13 +12,20 @@ def format_dependency(modname: str) -> T
     """Return (name, info) for given module.
 
     If possible, info is the path to the module's package."""
+    import importlib.metadata
+
     try:
         module = importlib.import_module(modname)
     except ImportError:
         return modname, 'not installed'
 
-    version = getattr(module, '__version__', '?')
+    if modname == 'flask':
+        version = importlib.metadata.version('flask')
+    else:
+        version = getattr(module, '__version__', '?')
+
     name = f'{modname}-{version}'
+
     if modname == 'ase':
         githash = search_current_git_hash(module)
         if githash:
diff -pruN 3.24.0-1/ase/dft/band_structure.py 3.26.0-1/ase/dft/band_structure.py
--- 3.24.0-1/ase/dft/band_structure.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/band_structure.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import warnings
 
 try:
diff -pruN 3.24.0-1/ase/dft/bandgap.py 3.26.0-1/ase/dft/bandgap.py
--- 3.24.0-1/ase/dft/bandgap.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/bandgap.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import warnings
 from dataclasses import dataclass
 
diff -pruN 3.24.0-1/ase/dft/bee.py 3.26.0-1/ase/dft/bee.py
--- 3.24.0-1/ase/dft/bee.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/bee.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 from typing import Any, Union
 
diff -pruN 3.24.0-1/ase/dft/bz.py 3.26.0-1/ase/dft/bz.py
--- 3.24.0-1/ase/dft/bz.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/bz.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from itertools import product
 from math import cos, pi, sin
 from typing import Any, Dict, Optional, Tuple, Union
@@ -340,8 +342,6 @@ def bz_plot(cell: Cell, vectors: bool =
 
             for name, point in zip(names, points):
                 name = normalize_name(name)
-                for transform in transforms:
-                    point = transform.apply(point)
                 point = point[:plotter.axis_dim]
                 ax.text(*point, rf'$\mathrm{{{name}}}$',
                         color='g', **plotter.label_options(point))
diff -pruN 3.24.0-1/ase/dft/dos.py 3.26.0-1/ase/dft/dos.py
--- 3.24.0-1/ase/dft/dos.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/dos.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from math import pi, sqrt
 
 import numpy as np
diff -pruN 3.24.0-1/ase/dft/kpoints.py 3.26.0-1/ase/dft/kpoints.py
--- 3.24.0-1/ase/dft/kpoints.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/kpoints.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 import warnings
 from typing import Dict
diff -pruN 3.24.0-1/ase/dft/pars_beefvdw.py 3.26.0-1/ase/dft/pars_beefvdw.py
--- 3.24.0-1/ase/dft/pars_beefvdw.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/pars_beefvdw.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 # flake8: noqa
diff -pruN 3.24.0-1/ase/dft/pars_mbeef.py 3.26.0-1/ase/dft/pars_mbeef.py
--- 3.24.0-1/ase/dft/pars_mbeef.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/pars_mbeef.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 # flake8: noqa
diff -pruN 3.24.0-1/ase/dft/pars_mbeefvdw.py 3.26.0-1/ase/dft/pars_mbeefvdw.py
--- 3.24.0-1/ase/dft/pars_mbeefvdw.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/pars_mbeefvdw.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 # flake8: noqa
diff -pruN 3.24.0-1/ase/dft/stm.py 3.26.0-1/ase/dft/stm.py
--- 3.24.0-1/ase/dft/stm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/stm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.io.jsonio import read_json, write_json
diff -pruN 3.24.0-1/ase/dft/wannier.py 3.26.0-1/ase/dft/wannier.py
--- 3.24.0-1/ase/dft/wannier.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/wannier.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,8 +1,9 @@
-""" Partly occupied Wannier functions
+"""Partly occupied Wannier functions
 
-    Find the set of partly occupied Wannier functions using the method from
-    Thygesen, Hansen and Jacobsen PRB v72 i12 p125119 2005.
+Find the set of partly occupied Wannier functions using the method from
+Thygesen, Hansen and Jacobsen PRB v72 i12 p125119 2005.
 """
+
 import functools
 import warnings
 from math import pi, sqrt
@@ -34,8 +35,8 @@ def gram_schmidt(U):
 
 def lowdin(U, S=None):
     """Orthonormalize columns of U according to the symmetric Lowdin procedure.
-       The implementation uses SVD, like symm. Lowdin it returns the nearest
-       orthonormal matrix, but is more robust.
+    The implementation uses SVD, like symm. Lowdin it returns the nearest
+    orthonormal matrix, but is more robust.
     """
 
     L, _s, R = np.linalg.svd(U, full_matrices=False)
@@ -45,22 +46,36 @@ def lowdin(U, S=None):
 def neighbor_k_search(k_c, G_c, kpt_kc, tol=1e-4):
     # search for k1 (in kpt_kc) and k0 (in alldir), such that
     # k1 - k - G + k0 = 0
-    alldir_dc = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1],
-                          [1, 1, 0], [1, 0, 1], [0, 1, 1]], dtype=int)
+    alldir_dc = np.array(
+        [
+            [0, 0, 0],
+            [1, 0, 0],
+            [0, 1, 0],
+            [0, 0, 1],
+            [1, 1, 0],
+            [1, 0, 1],
+            [0, 1, 1],
+        ],
+        dtype=int,
+    )
     for k0_c in alldir_dc:
         for k1, k1_c in enumerate(kpt_kc):
             if np.linalg.norm(k1_c - k_c - G_c + k0_c) < tol:
                 return k1, k0_c
 
-    raise ValueError(f'Wannier: Did not find matching kpoint for kpt={k_c}.  '
-                     'Probably non-uniform k-point grid')
+    raise ValueError(
+        f'Wannier: Did not find matching kpoint for kpt={k_c}.  '
+        'Probably non-uniform k-point grid'
+    )
 
 
 def calculate_weights(cell_cc, normalize=True):
-    """ Weights are used for non-cubic cells, see PRB **61**, 10040
-        If normalized they lose the physical dimension."""
-    alldirs_dc = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1],
-                           [1, 1, 0], [1, 0, 1], [0, 1, 1]], dtype=int)
+    """Weights are used for non-cubic cells, see PRB **61**, 10040
+    If normalized they lose the physical dimension."""
+    alldirs_dc = np.array(
+        [[1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 1, 0], [1, 0, 1], [0, 1, 1]],
+        dtype=int,
+    )
     g = cell_cc @ cell_cc.T
     # NOTE: Only first 3 of following 6 weights are presently used:
     w = np.zeros(6)
@@ -76,15 +91,15 @@ def calculate_weights(cell_cc, normalize
     weight_d = w[:3]
     for d in range(3, 6):
         if abs(w[d]) > 1e-5:
-            Gdir_dc = np.concatenate((Gdir_dc, alldirs_dc[d:d + 1]))
-            weight_d = np.concatenate((weight_d, w[d:d + 1]))
+            Gdir_dc = np.concatenate((Gdir_dc, alldirs_dc[d : d + 1]))
+            weight_d = np.concatenate((weight_d, w[d : d + 1]))
     if normalize:
         weight_d /= max(abs(weight_d))
     return weight_d, Gdir_dc
 
 
-def steepest_descent(func, step=.005, tolerance=1e-6, log=silent, **kwargs):
-    fvalueold = 0.
+def steepest_descent(func, step=0.005, tolerance=1e-6, log=silent, **kwargs):
+    fvalueold = 0.0
     fvalue = fvalueold + 10
     count = 0
     while abs((fvalue - fvalueold) / fvalue) > tolerance:
@@ -96,14 +111,14 @@ def steepest_descent(func, step=.005, to
         log(f'SteepestDescent: iter={count}, value={fvalue}')
 
 
-def md_min(func, step=.25, tolerance=1e-6, max_iter=10000,
-           log=silent, **kwargs):
-
+def md_min(
+    func, step=0.25, tolerance=1e-6, max_iter=10000, log=silent, **kwargs
+):
     log('Localize with step =', step, 'and tolerance =', tolerance)
     finit = func.get_functional_value()
 
     t = -time()
-    fvalueold = 0.
+    fvalueold = 0.0
     fvalue = fvalueold + 10
     count = 0
     V = np.zeros(func.get_gradients().shape, dtype=complex)
@@ -123,14 +138,18 @@ def md_min(func, step=.25, tolerance=1e-
         log(f'MDmin: iter={count}, step={step}, value={fvalue}')
         if count > max_iter:
             t += time()
-            warnings.warn('Max iterations reached: '
-                          'iters=%s, step=%s, seconds=%0.2f, value=%0.4f'
-                          % (count, step, t, fvalue.real))
+            warnings.warn(
+                'Max iterations reached: '
+                'iters=%s, step=%s, seconds=%0.2f, value=%0.4f'
+                % (count, step, t, fvalue.real)
+            )
             break
 
     t += time()
-    log('%d iterations in %0.2f seconds (%0.2f ms/iter), endstep = %s' %
-        (count, t, t * 1000. / count, step))
+    log(
+        '%d iterations in %0.2f seconds (%0.2f ms/iter), endstep = %s'
+        % (count, t, t * 1000.0 / count, step)
+    )
     log(f'Initial value={finit}, Final value={fvalue}')
 
 
@@ -171,7 +190,7 @@ def rotation_from_projection(proj_nw, fi
         # Sort columns of eigenvectors matrix according to the eigenvalues
         # magnitude, select only the L largest ones. Then use them to obtain
         # the parameter C matrix.
-        C_ul[:] = proj_uw @ C_ww[:, np.argsort(- eig_w.real)[:L]]
+        C_ul[:] = proj_uw @ C_ww[:, np.argsort(-eig_w.real)[:L]]
 
         # Compute the section of the rotation matrix about 'non fixed' states
         U_ww[M:] = dag(C_ul) @ proj_uw
@@ -224,14 +243,18 @@ def scdm(pseudo_nkG, kpts, fixed_k, Nw):
     C_kul = []
 
     # compute factorization only at Gamma point
-    _, _, P = qr(pseudo_nkG[:, gamma_idx, :], mode='full',
-                 pivoting=True, check_finite=True)
+    _, _, P = qr(
+        pseudo_nkG[:, gamma_idx, :],
+        mode='full',
+        pivoting=True,
+        check_finite=True,
+    )
 
     for k in range(Nk):
         A_nw = pseudo_nkG[:, k, P[:Nw]]
-        U_ww, C_ul = rotation_from_projection(proj_nw=A_nw,
-                                              fixed=fixed_k[k],
-                                              ortho=True)
+        U_ww, C_ul = rotation_from_projection(
+            proj_nw=A_nw, fixed=fixed_k[k], ortho=True
+        )
         U_kww.append(U_ww)
         C_kul.append(C_ul)
 
@@ -261,9 +284,7 @@ def arbitrary_s_orbitals(atoms, Ns, rng=
             tmp_atoms.set_scaled_positions(s_pos)
 
             # Use dummy H atom to measure distance from any other atom
-            dists = tmp_atoms.get_distances(
-                a=-1,
-                indices=range(len(atoms)))
+            dists = tmp_atoms.get_distances(a=-1, indices=range(len(atoms)))
 
             # Check if it is close to at least one atom
             if (dists < 1.5).any():
@@ -285,8 +306,12 @@ def init_orbitals(atoms, ntot, rng=np.ra
 
     # List all the elements that should have occupied d-orbitals
     # in the valence states (according to GPAW setups)
-    d_metals = set(list(range(21, 31)) + list(range(39, 52)) +
-                   list(range(57, 84)) + list(range(89, 113)))
+    d_metals = set(
+        list(range(21, 31))
+        + list(range(39, 52))
+        + list(range(57, 84))
+        + list(range(89, 113))
+    )
     orbs = []
 
     # Start with zero orbitals
@@ -314,7 +339,7 @@ def square_modulus_of_Z_diagonal(Z_dww):
     Square modulus of the Z matrix diagonal, the diagonal is taken
     for the indexes running on the WFs.
     """
-    return np.abs(Z_dww.diagonal(0, 1, 2))**2
+    return np.abs(Z_dww.diagonal(0, 1, 2)) ** 2
 
 
 def get_kklst(kpt_kc, Gdir_dc):
@@ -343,8 +368,7 @@ def get_kklst(kpt_kc, Gdir_dc):
             slist = np.argsort(kpt_kc[:, c], kind='mergesort')
             skpoints_kc = np.take(kpt_kc, slist, axis=0)
             kdist_c[c] = max(
-                skpoints_kc[n + 1, c] - skpoints_kc[n, c]
-                for n in range(Nk - 1)
+                skpoints_kc[n + 1, c] - skpoints_kc[n, c] for n in range(Nk - 1)
             )
 
         for d, Gdir_c in enumerate(Gdir_dc):
@@ -355,8 +379,9 @@ def get_kklst(kpt_kc, Gdir_dc):
                     kklst_dk[d, k] = k
                     k0_dkc[d, k] = Gdir_c
                 else:
-                    kklst_dk[d, k], k0_dkc[d, k] = \
-                        neighbor_k_search(k_c, G_c, kpt_kc)
+                    kklst_dk[d, k], k0_dkc[d, k] = neighbor_k_search(
+                        k_c, G_c, kpt_kc
+                    )
     return kklst_dk, k0_dkc
 
 
@@ -370,7 +395,6 @@ def get_invkklst(kklst_dk):
 
 
 def choose_states(calcdata, fixedenergy, fixedstates, Nk, nwannier, log, spin):
-
     if fixedenergy is None and fixedstates is not None:
         if isinstance(fixedstates, int):
             fixedstates = [fixedstates] * Nk
@@ -392,16 +416,17 @@ def choose_states(calcdata, fixedenergy,
             tmp_fixedstates_k.append(kindex)
         fixedstates_k = np.array(tmp_fixedstates_k, int)
     elif fixedenergy is not None and fixedstates is not None:
-        raise RuntimeError(
-            'You can not set both fixedenergy and fixedstates')
+        raise RuntimeError('You can not set both fixedenergy and fixedstates')
 
     if nwannier == 'auto':
         if fixedenergy is None and fixedstates is None:
             # Assume the fixedexergy parameter equal to 0 and
             # find the states below the Fermi level at each k-point.
-            log("nwannier=auto but no 'fixedenergy' or 'fixedstates'",
-                "parameter was provided, using Fermi level as",
-                "energy cutoff.")
+            log(
+                "nwannier=auto but no 'fixedenergy' or 'fixedstates'",
+                'parameter was provided, using Fermi level as',
+                'energy cutoff.',
+            )
             tmp_fixedstates_k = []
             for k in range(Nk):
                 eps_n = calcdata.eps_skn[spin, k]
@@ -430,8 +455,7 @@ def get_eigenvalues(calc):
 
 
 class CalcData:
-    def __init__(self, kpt_kc, atoms, fermi_level, lumo, eps_skn,
-                 gap):
+    def __init__(self, kpt_kc, atoms, fermi_level, lumo, eps_skn, gap):
         self.kpt_kc = kpt_kc
         self.atoms = atoms
         self.fermi_level = fermi_level
@@ -447,7 +471,12 @@ class CalcData:
 def get_calcdata(calc):
     kpt_kc = calc.get_bz_k_points()
     # Make sure there is no symmetry reduction
-    assert len(calc.get_ibz_k_points()) == len(kpt_kc)
+    if len(calc.get_ibz_k_points()) != len(kpt_kc):
+        raise RuntimeError(
+            'K-point symmetry is not currently supported. '
+            "Please re-run your calculator with symmetry='off'."
+        )
+
     lumo = calc.get_homo_lumo()[1]
     gap = bandgap(calc=calc)[0]
     return CalcData(
@@ -456,7 +485,8 @@ def get_calcdata(calc):
         fermi_level=calc.get_fermi_level(),
         lumo=lumo,
         eps_skn=get_eigenvalues(calc),
-        gap=gap)
+        gap=gap,
+    )
 
 
 class Wannier:
@@ -466,16 +496,20 @@ class Wannier:
     Thygesen, Hansen and Jacobsen PRB v72 i12 p125119 2005.
     """
 
-    def __init__(self, nwannier, calc,
-                 file=None,
-                 nbands=None,
-                 fixedenergy=None,
-                 fixedstates=None,
-                 spin=0,
-                 initialwannier='orbitals',
-                 functional='std',
-                 rng=np.random,
-                 log=silent):
+    def __init__(
+        self,
+        nwannier,
+        calc,
+        file=None,
+        nbands=None,
+        fixedenergy=None,
+        fixedstates=None,
+        spin=0,
+        initialwannier='orbitals',
+        functional='std',
+        rng=np.random,
+        log=silent,
+    ):
         """
         Required arguments:
 
@@ -528,7 +562,7 @@ class Wannier:
           ``rng``: Random number generator for ``initialwannier``.
 
           ``log``: Function which logs, such as print().
-          """
+        """
         # Bloch phase sign convention.
         # May require special cases depending on which code is used.
         sign = -1
@@ -557,8 +591,14 @@ class Wannier:
         self.nbands = nbands
 
         self.fixedstates_k, self.nwannier = choose_states(
-            self.calcdata, fixedenergy, fixedstates, self.Nk, nwannier,
-            log, spin)
+            self.calcdata,
+            fixedenergy,
+            fixedstates,
+            self.Nk,
+            nwannier,
+            log,
+            spin,
+        )
 
         # Compute the number of extra degrees of freedom (EDF)
         self.edf_k = self.nwannier - self.fixedstates_k
@@ -604,8 +644,13 @@ class Wannier:
                 k1 = self.kklst_dk[d, k]
                 k0_c = k0_dkc[d, k]
                 Z_dknn[d, k] = calc.get_wannier_localization_matrix(
-                    nbands=Nb, dirG=dirG, kpoint=k, nextkpoint=k1,
-                    G_I=k0_c, spin=self.spin)
+                    nbands=Nb,
+                    dirG=dirG,
+                    kpoint=k,
+                    nextkpoint=k1,
+                    G_I=k0_c,
+                    spin=self.spin,
+                )
         return Z_dknn
 
     @property
@@ -627,8 +672,9 @@ class Wannier:
         """
         from ase.dft.wannierstate import WannierSpec, WannierState
 
-        spec = WannierSpec(self.Nk, self.nwannier, self.nbands,
-                           self.fixedstates_k)
+        spec = WannierSpec(
+            self.Nk, self.nwannier, self.nbands, self.fixedstates_k
+        )
 
         if file is not None:
             with paropen(file, 'r') as fd:
@@ -643,13 +689,14 @@ class Wannier:
         elif initialwannier == 'orbitals':
             orbitals = init_orbitals(self.atoms, self.nwannier, rng)
             wannier_state = spec.initial_orbitals(
-                self.calc, orbitals, self.kptgrid, self.edf_k, self.spin)
+                self.calc, orbitals, self.kptgrid, self.edf_k, self.spin
+            )
         elif initialwannier == 'scdm':
             wannier_state = spec.scdm(self.calc, self.kpt_kc, self.spin)
         else:
             wannier_state = spec.initial_wannier(
-                self.calc, initialwannier, self.kptgrid,
-                self.edf_k, self.spin)
+                self.calc, initialwannier, self.kptgrid, self.edf_k, self.spin
+            )
 
         self.wannier_state = wannier_state
         self.update()
@@ -672,8 +719,9 @@ class Wannier:
         for d in range(self.Ndir):
             for k in range(self.Nk):
                 k1 = self.kklst_dk[d, k]
-                self.Z_dkww[d, k] = dag(self.V_knw[k]) \
-                    @ (self.Z_dknn[d, k] @ self.V_knw[k1])
+                self.Z_dkww[d, k] = dag(self.V_knw[k]) @ (
+                    self.Z_dknn[d, k] @ self.V_knw[k1]
+                )
 
         # Update the new Z matrix
         self.Z_dww = self.Z_dkww.sum(axis=1) / self.Nk
@@ -700,8 +748,9 @@ class Wannier:
         # available bands we have.
         max_number_fixedstates = np.max(self.fixedstates_k)
 
-        min_range_value = max(self.nwannier - int(np.floor(nwrange / 2)),
-                              max_number_fixedstates)
+        min_range_value = max(
+            self.nwannier - int(np.floor(nwrange / 2)), max_number_fixedstates
+        )
         max_range_value = min(min_range_value + nwrange, self.nbands + 1)
         Nws = np.arange(min_range_value, max_range_value)
 
@@ -717,14 +766,16 @@ class Wannier:
 
             # Define once with the fastest 'initialwannier',
             # then initialize with random seeds in the for loop
-            wan = Wannier(nwannier=int(Nw),
-                          calc=self.calc,
-                          nbands=self.nbands,
-                          spin=self.spin,
-                          functional=self.functional,
-                          initialwannier='bloch',
-                          log=self.log,
-                          rng=self.rng)
+            wan = Wannier(
+                nwannier=int(Nw),
+                calc=self.calc,
+                nbands=self.nbands,
+                spin=self.spin,
+                functional=self.functional,
+                initialwannier='bloch',
+                log=self.log,
+                rng=self.rng,
+            )
             wan.fixedstates_k = self.fixedstates_k
             wan.edf_k = wan.nwannier - wan.fixedstates_k
 
@@ -766,8 +817,9 @@ class Wannier:
         Note that this function can fail with some Bravais lattices,
         see `get_spreads()` for a more robust alternative.
         """
-        r2 = - (self.largeunitcell_cc.diagonal()**2 / (2 * pi)**2) \
-            @ np.log(abs(self.Z_dww[:3].diagonal(0, 1, 2))**2)
+        r2 = -(self.largeunitcell_cc.diagonal() ** 2 / (2 * pi) ** 2) @ np.log(
+            abs(self.Z_dww[:3].diagonal(0, 1, 2)) ** 2
+        )
         return np.sqrt(r2)
 
     def get_spreads(self):
@@ -785,11 +837,11 @@ class Wannier:
         # compute weights without normalization, to keep physical dimension
         weight_d, _ = calculate_weights(self.largeunitcell_cc, normalize=False)
         Z2_dw = square_modulus_of_Z_diagonal(self.Z_dww)
-        spread_w = - (np.log(Z2_dw).T @ weight_d).real / (2 * np.pi)**2
+        spread_w = -(np.log(Z2_dw).T @ weight_d).real / (2 * np.pi) ** 2
         return spread_w
 
     def get_spectral_weight(self, w):
-        return abs(self.V_knw[:, :, w])**2 / self.Nk
+        return abs(self.V_knw[:, :, w]) ** 2 / self.Nk
 
     def get_pdos(self, w, energies, width):
         """Projected density of states (PDOS).
@@ -805,8 +857,8 @@ class Wannier:
             eig_n = self.calcdata.eps_skn[self.spin, k]
             for weight, eig in zip(spec_n, eig_n):
                 # Add gaussian centered at the eigenvalue
-                x = ((energies - eig) / width)**2
-                dos += weight * np.exp(-x.clip(0., 40.)) / (sqrt(pi) * width)
+                x = ((energies - eig) / width) ** 2
+                dos += weight * np.exp(-x.clip(0.0, 40.0)) / (sqrt(pi) * width)
         return dos
 
     def translate(self, w, R):
@@ -816,7 +868,7 @@ class Wannier:
         vectors of the small cell.
         """
         for kpt_c, U_ww in zip(self.kpt_kc, self.U_kww):
-            U_ww[:, w] *= np.exp(2.j * pi * (np.array(R) @ kpt_c))
+            U_ww[:, w] *= np.exp(2.0j * pi * (np.array(R) @ kpt_c))
         self.update()
 
     def translate_to_cell(self, w, cell):
@@ -842,11 +894,14 @@ class Wannier:
         the orbitals to the cell [2,2,2].  In this way the pbc
         boundary conditions will not be noticed.
         """
-        scaled_wc = (np.angle(self.Z_dww[:3].diagonal(0, 1, 2)).T *
-                     self.kptgrid / (2 * pi))
+        scaled_wc = (
+            np.angle(self.Z_dww[:3].diagonal(0, 1, 2)).T
+            * self.kptgrid
+            / (2 * pi)
+        )
         trans_wc = np.array(cell)[None] - np.floor(scaled_wc)
         for kpt_c, U_ww in zip(self.kpt_kc, self.U_kww):
-            U_ww *= np.exp(2.j * pi * (trans_wc @ kpt_c))
+            U_ww *= np.exp(2.0j * pi * (trans_wc @ kpt_c))
         self.update()
 
     def distances(self, R):
@@ -866,7 +921,7 @@ class Wannier:
             r2 += self.unitcell_cc[i] * R[i]
 
         r2 = np.swapaxes(r2.repeat(Nw, axis=0).reshape(Nw, Nw, 3), 0, 1)
-        return np.sqrt(np.sum((r1 - r2)**2, axis=-1))
+        return np.sqrt(np.sum((r1 - r2) ** 2, axis=-1))
 
     @functools.lru_cache(maxsize=10000)
     def _get_hopping(self, n1, n2, n3):
@@ -885,7 +940,7 @@ class Wannier:
         R = np.array([n1, n2, n3], float)
         H_ww = np.zeros([self.nwannier, self.nwannier], complex)
         for k, kpt_c in enumerate(self.kpt_kc):
-            phase = np.exp(-2.j * pi * (np.array(R) @ kpt_c))
+            phase = np.exp(-2.0j * pi * (np.array(R) @ kpt_c))
             H_ww += self.get_hamiltonian(k) * phase
         return H_ww / self.Nk
 
@@ -912,7 +967,7 @@ class Wannier:
           H(k) = V    diag(eps )  V
                   k           k    k
         """
-        eps_n = self.calcdata.eps_skn[self.spin, k, :self.nbands]
+        eps_n = self.calcdata.eps_skn[self.spin, k, : self.nbands]
         V_nw = self.V_knw[k]
         return (dag(V_nw) * eps_n) @ V_nw
 
@@ -937,7 +992,7 @@ class Wannier:
                 for n3 in range(-N3, N3 + 1):
                     R = np.array([n1, n2, n3], float)
                     hop_ww = self.get_hopping(R)
-                    phase = np.exp(+2.j * pi * (R @ kpt_c))
+                    phase = np.exp(+2.0j * pi * (R @ kpt_c))
                     Hk += hop_ww * phase
         return Hk
 
@@ -979,16 +1034,19 @@ class Wannier:
             wan_G = np.zeros(dim, complex)
             for n, coeff in enumerate(vec_n):
                 wan_G += coeff * self.calc.get_pseudo_wave_function(
-                    n, k, self.spin, pad=True)
+                    n, k, self.spin, pad=True
+                )
 
             # Distribute the small wavefunction over large cell:
             for n1 in range(N1):
                 for n2 in range(N2):
                     for n3 in range(N3):  # sign?
-                        e = np.exp(-2.j * pi * np.array([n1, n2, n3]) @ kpt_c)
-                        wanniergrid[n1 * dim[0]:(n1 + 1) * dim[0],
-                                    n2 * dim[1]:(n2 + 1) * dim[1],
-                                    n3 * dim[2]:(n3 + 1) * dim[2]] += e * wan_G
+                        e = np.exp(-2.0j * pi * np.array([n1, n2, n3]) @ kpt_c)
+                        wanniergrid[
+                            n1 * dim[0] : (n1 + 1) * dim[0],
+                            n2 * dim[1] : (n2 + 1) * dim[1],
+                            n3 * dim[2] : (n3 + 1) * dim[2],
+                        ] += e * wan_G
 
         # Normalization
         wanniergrid /= np.sqrt(self.Nk)
@@ -1026,17 +1084,24 @@ class Wannier:
             data = np.angle(func)
         else:
             if self.Nk == 1:
-                func *= np.exp(-1.j * np.angle(func.max()))
+                func *= np.exp(-1.0j * np.angle(func.max()))
             func = abs(func)
             data = func
 
         write(fname, atoms, data=data, format='cube')
 
-    def localize(self, step=0.25, tolerance=1e-08,
-                 updaterot=True, updatecoeff=True):
+    def localize(
+        self, step=0.25, tolerance=1e-08, updaterot=True, updatecoeff=True
+    ):
         """Optimize rotation to give maximal localization"""
-        md_min(self, step=step, tolerance=tolerance, log=self.log,
-               updaterot=updaterot, updatecoeff=updatecoeff)
+        md_min(
+            self,
+            step=step,
+            tolerance=tolerance,
+            log=self.log,
+            updaterot=updaterot,
+            updatecoeff=updatecoeff,
+        )
 
     def get_functional_value(self):
         """Calculate the value of the spread functional.
@@ -1060,8 +1125,10 @@ class Wannier:
             fun = np.sum(a_w)
         elif self.functional == 'var':
             fun = np.sum(a_w) - self.nwannier * np.var(a_w)
-            self.log(f'std: {np.sum(a_w):.4f}',
-                     f'\tvar: {self.nwannier * np.var(a_w):.4f}')
+            self.log(
+                f'std: {np.sum(a_w):.4f}',
+                f'\tvar: {self.nwannier * np.var(a_w):.4f}',
+            )
         return fun
 
     def get_gradients(self):
@@ -1125,25 +1192,36 @@ class Wannier:
 
                 if L > 0:
                     Ctemp_nw += weight * (
-                        ((Z_knn[k] @ V_knw[k1]) * diagZ_w.conj() +
-                         (dag(Z_knn[k2]) @ V_knw[k2]) * diagZ_w) @ dag(U_ww))
+                        (
+                            (Z_knn[k] @ V_knw[k1]) * diagZ_w.conj()
+                            + (dag(Z_knn[k2]) @ V_knw[k2]) * diagZ_w
+                        )
+                        @ dag(U_ww)
+                    )
 
                     if self.functional == 'var':
                         # Gradient of the variance term, split in two terms
                         def variance_term_computer(factor):
                             result = (
-                                self.nwannier * 2 * weight * (
-                                    ((Z_knn[k] @ V_knw[k1]) * factor.conj() +
-                                     (dag(Z_knn[k2]) @ V_knw[k2]) * factor) @
-                                    dag(U_ww)) / Nw**2
+                                self.nwannier
+                                * 2
+                                * weight
+                                * (
+                                    (
+                                        (Z_knn[k] @ V_knw[k1]) * factor.conj()
+                                        + (dag(Z_knn[k2]) @ V_knw[k2]) * factor
+                                    )
+                                    @ dag(U_ww)
+                                )
+                                / Nw**2
                             )
                             return result
 
-                        first_term = \
+                        first_term = (
                             O_sum * variance_term_computer(diagZ_w) / Nw**2
+                        )
 
-                        second_term = \
-                            - variance_term_computer(diagOZ_w) / Nw
+                        second_term = -variance_term_computer(diagOZ_w) / Nw
 
                         Ctemp_nw += first_term + second_term
 
@@ -1151,13 +1229,21 @@ class Wannier:
                 Utemp_ww += weight * (temp - dag(temp))
 
                 if self.functional == 'var':
-                    Utemp_ww += (self.nwannier * 2 * O_sum * weight *
-                                 (temp - dag(temp)) / Nw**2)
-
-                    temp = (OZii_ww.T * Z_kww[k].conj()
-                            - OZii_ww * Z_kww[k2].conj())
-                    Utemp_ww -= (self.nwannier * 2 * weight *
-                                 (temp - dag(temp)) / Nw)
+                    Utemp_ww += (
+                        self.nwannier
+                        * 2
+                        * O_sum
+                        * weight
+                        * (temp - dag(temp))
+                        / Nw**2
+                    )
+
+                    temp = (
+                        OZii_ww.T * Z_kww[k].conj() - OZii_ww * Z_kww[k2].conj()
+                    )
+                    Utemp_ww -= (
+                        self.nwannier * 2 * weight * (temp - dag(temp)) / Nw
+                    )
 
             dU.append(Utemp_ww.ravel())
 
@@ -1174,8 +1260,7 @@ class Wannier:
         """
         Compute the contribution of each WF to the spread functional.
         """
-        return (square_modulus_of_Z_diagonal(self.Z_dww).T
-                @ self.weight_d).real
+        return (square_modulus_of_Z_diagonal(self.Z_dww).T @ self.weight_d).real
 
     def step(self, dX, updaterot=True, updatecoeff=True):
         # dX is (A, dC) where U->Uexp(-A) and C->C+dC
@@ -1184,13 +1269,13 @@ class Wannier:
         M_k = self.fixedstates_k
         L_k = self.edf_k
         if updaterot:
-            A_kww = dX[:Nk * Nw**2].reshape(Nk, Nw, Nw)
+            A_kww = dX[: Nk * Nw**2].reshape(Nk, Nw, Nw)
             for U, A in zip(self.U_kww, A_kww):
-                H = -1.j * A.conj()
+                H = -1.0j * A.conj()
                 epsilon, Z = np.linalg.eigh(H)
                 # Z contains the eigenvectors as COLUMNS.
                 # Since H = iA, dU = exp(-A) = exp(iH) = ZDZ^d
-                dU = Z * np.exp(1.j * epsilon) @ dag(Z)
+                dU = Z * np.exp(1.0j * epsilon) @ dag(Z)
                 if U.dtype == float:
                     U[:] = (U @ dU).real
                 else:
@@ -1202,7 +1287,7 @@ class Wannier:
                 if L == 0 or unocc == 0:
                     continue
                 Ncoeff = L * unocc
-                deltaC = dX[Nk * Nw**2 + start: Nk * Nw**2 + start + Ncoeff]
+                deltaC = dX[Nk * Nw**2 + start : Nk * Nw**2 + start + Ncoeff]
                 C += deltaC.reshape(unocc, L)
                 gram_schmidt(C)
                 start += Ncoeff
diff -pruN 3.24.0-1/ase/dft/wannierstate.py 3.26.0-1/ase/dft/wannierstate.py
--- 3.24.0-1/ase/dft/wannierstate.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/dft/wannierstate.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 from scipy.linalg import qr
 
diff -pruN 3.24.0-1/ase/eos.py 3.26.0-1/ase/eos.py
--- 3.24.0-1/ase/eos.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/eos.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import warnings
 
 import numpy as np
@@ -413,7 +415,7 @@ def calculate_eos(atoms, npoints=5, eps=
 class CLICommand:
     """Calculate EOS from one or more trajectory files.
 
-    See https://wiki.fysik.dtu.dk/ase/tutorials/eos/eos.html for
+    See https://ase-lib.org/tutorials/eos/eos.html for
     more information.
     """
 
diff -pruN 3.24.0-1/ase/filters.py 3.26.0-1/ase/filters.py
--- 3.24.0-1/ase/filters.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/filters.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,7 @@
+# fmt: off
+
 """Filters"""
+from functools import cached_property
 from itertools import product
 from warnings import warn
 
@@ -6,7 +9,7 @@ import numpy as np
 
 from ase.calculators.calculator import PropertyNotImplementedError
 from ase.stress import full_3x3_to_voigt_6_stress, voigt_6_to_full_3x3_stress
-from ase.utils import deprecated, lazyproperty
+from ase.utils import deprecated
 from ase.utils.abc import Optimizable
 
 __all__ = [
@@ -19,16 +22,16 @@ class OptimizableFilter(Optimizable):
     def __init__(self, filterobj):
         self.filterobj = filterobj
 
-    def get_positions(self):
-        return self.filterobj.get_positions()
+    def get_x(self):
+        return self.filterobj.get_positions().ravel()
 
-    def set_positions(self, positions):
-        self.filterobj.set_positions(positions)
+    def set_x(self, x):
+        self.filterobj.set_positions(x.reshape(-1, 3))
 
-    def get_forces(self):
-        return self.filterobj.get_forces()
+    def get_gradient(self):
+        return self.filterobj.get_forces().ravel()
 
-    @lazyproperty
+    @cached_property
     def _use_force_consistent_energy(self):
         # This boolean is in principle invalidated if the
         # calculator changes.  This can lead to weird things
@@ -40,13 +43,13 @@ class OptimizableFilter(Optimizable):
         else:
             return True
 
-    def get_potential_energy(self):
+    def get_value(self):
         force_consistent = self._use_force_consistent_energy
         return self.filterobj.get_potential_energy(
             force_consistent=force_consistent)
 
-    def __len__(self):
-        return len(self.filterobj)
+    def ndofs(self):
+        return 3 * len(self.filterobj)
 
     def iterimages(self):
         return self.filterobj.iterimages()
@@ -421,17 +424,20 @@ class UnitCellFilter(Filter):
         natoms = len(self.atoms)
         new_atom_positions = new[:natoms]
         new_deform_grad = new[natoms:] / self.cell_factor
+        deform = (new_deform_grad - np.eye(3)).T * self.mask
         # Set the new cell from the original cell and the new
         # deformation gradient.  Both current and final structures should
         # preserve symmetry, so if set_cell() calls FixSymmetry.adjust_cell(),
         # it should be OK
-        self.atoms.set_cell(self.orig_cell @ new_deform_grad.T,
+        newcell = self.orig_cell @ (np.eye(3) + deform)
+
+        self.atoms.set_cell(newcell,
                             scale_atoms=True)
         # Set the positions from the ones passed in (which are without the
         # deformation gradient applied) and the new deformation gradient.
         # This should also preserve symmetry, so if set_positions() calls
         # FixSymmetyr.adjust_positions(), it should be OK
-        self.atoms.set_positions(new_atom_positions @ new_deform_grad.T,
+        self.atoms.set_positions(new_atom_positions @ (np.eye(3) + deform),
                                  **kwargs)
 
     def get_potential_energy(self, force_consistent=True):
diff -pruN 3.24.0-1/ase/formula.py 3.26.0-1/ase/formula.py
--- 3.24.0-1/ase/formula.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/formula.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 from functools import lru_cache
 from math import gcd
diff -pruN 3.24.0-1/ase/ga/bulk_crossovers.py 3.26.0-1/ase/ga/bulk_crossovers.py
--- 3.24.0-1/ase/ga/bulk_crossovers.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/bulk_crossovers.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,5 +0,0 @@
-raise ImportError(
-    'The ase.ga.bulk_crossovers module has been deprecated. '
-    'The same functionality is now provided by the '
-    'ase.ga.cutandsplicepairing module. Please consult its documentation '
-    'to verify how to e.g. initialize a CutAndSplicePairing object.')
diff -pruN 3.24.0-1/ase/ga/bulk_mutations.py 3.26.0-1/ase/ga/bulk_mutations.py
--- 3.24.0-1/ase/ga/bulk_mutations.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/bulk_mutations.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,6 +0,0 @@
-raise ImportError(
-    'The ase.ga.bulk_mutations module has been deprecated. '
-    'The same functionality is now provided by the '
-    'ase.ga.standardmutations and ase.ga.soft_mutation modules. '
-    'Please consult their documentation to verify how to initialize '
-    'the different mutation operators.')
diff -pruN 3.24.0-1/ase/ga/bulk_startgenerator.py 3.26.0-1/ase/ga/bulk_startgenerator.py
--- 3.24.0-1/ase/ga/bulk_startgenerator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/bulk_startgenerator.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,5 +0,0 @@
-raise ImportError(
-    'The ase.ga.bulk_startgenerator module has been deprecated. '
-    'The same functionality is now provided by the '
-    'ase.ga.startgenerator module. Please consult its documentation '
-    'to verify how to e.g. initialize a StartGenerator object.')
diff -pruN 3.24.0-1/ase/ga/bulk_utilities.py 3.26.0-1/ase/ga/bulk_utilities.py
--- 3.24.0-1/ase/ga/bulk_utilities.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/bulk_utilities.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,4 +0,0 @@
-raise ImportError(
-    'The ase.ga.bulk_utilities module has been deprecated. '
-    'The same functionality is now provided by the '
-    'ase.ga.utilities module.')
diff -pruN 3.24.0-1/ase/ga/convergence.py 3.26.0-1/ase/ga/convergence.py
--- 3.24.0-1/ase/ga/convergence.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/convergence.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Classes that determine convergence of an algorithm run
 based on population stagnation or max raw score reached"""
 from ase.ga import get_raw_score
diff -pruN 3.24.0-1/ase/ga/cutandsplicepairing.py 3.26.0-1/ase/ga/cutandsplicepairing.py
--- 3.24.0-1/ase/ga/cutandsplicepairing.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/cutandsplicepairing.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Implementation of the cut-and-splice paring operator."""
 import numpy as np
 
diff -pruN 3.24.0-1/ase/ga/data.py 3.26.0-1/ase/ga/data.py
--- 3.24.0-1/ase/ga/data.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/data.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
     Objects which handle all communication with the SQLite database.
 """
diff -pruN 3.24.0-1/ase/ga/element_crossovers.py 3.26.0-1/ase/ga/element_crossovers.py
--- 3.24.0-1/ase/ga/element_crossovers.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/element_crossovers.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Crossover classes, that cross the elements in the supplied
 atoms objects.
 
diff -pruN 3.24.0-1/ase/ga/element_mutations.py 3.26.0-1/ase/ga/element_mutations.py
--- 3.24.0-1/ase/ga/element_mutations.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/element_mutations.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Mutation classes, that mutate the elements in the supplied
 atoms objects."""
 import numpy as np
diff -pruN 3.24.0-1/ase/ga/multiprocessingrun.py 3.26.0-1/ase/ga/multiprocessingrun.py
--- 3.24.0-1/ase/ga/multiprocessingrun.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/multiprocessingrun.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ Class for handling several simultaneous jobs.
 The class has been tested on Niflheim-opteron4.
 """
diff -pruN 3.24.0-1/ase/ga/offspring_creator.py 3.26.0-1/ase/ga/offspring_creator.py
--- 3.24.0-1/ase/ga/offspring_creator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/offspring_creator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Base module for all operators that create offspring."""
 import numpy as np
 
diff -pruN 3.24.0-1/ase/ga/ofp_comparator.py 3.26.0-1/ase/ga/ofp_comparator.py
--- 3.24.0-1/ase/ga/ofp_comparator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/ofp_comparator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from itertools import combinations_with_replacement
 from math import erf
 
diff -pruN 3.24.0-1/ase/ga/parallellocalrun.py 3.26.0-1/ase/ga/parallellocalrun.py
--- 3.24.0-1/ase/ga/parallellocalrun.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/parallellocalrun.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ Class for handling several simultaneous jobs.
     The class has been tested on linux and Mac OS X.
 """
diff -pruN 3.24.0-1/ase/ga/particle_comparator.py 3.26.0-1/ase/ga/particle_comparator.py
--- 3.24.0-1/ase/ga/particle_comparator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/particle_comparator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Comparators originally meant to be used with particles"""
 import numpy as np
 
diff -pruN 3.24.0-1/ase/ga/particle_crossovers.py 3.26.0-1/ase/ga/particle_crossovers.py
--- 3.24.0-1/ase/ga/particle_crossovers.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/particle_crossovers.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from itertools import chain
 
 import numpy as np
diff -pruN 3.24.0-1/ase/ga/particle_mutations.py 3.26.0-1/ase/ga/particle_mutations.py
--- 3.24.0-1/ase/ga/particle_mutations.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/particle_mutations.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from operator import itemgetter
 
 import numpy as np
diff -pruN 3.24.0-1/ase/ga/pbs_queue_run.py 3.26.0-1/ase/ga/pbs_queue_run.py
--- 3.24.0-1/ase/ga/pbs_queue_run.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/pbs_queue_run.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ Class for handling interaction with the PBS queuing system."""
 import os
 import time
diff -pruN 3.24.0-1/ase/ga/population.py 3.26.0-1/ase/ga/population.py
--- 3.24.0-1/ase/ga/population.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/population.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ Implementation of a population for maintaining a GA population and
 proposing structures to pair. """
 from math import exp, sqrt, tanh
diff -pruN 3.24.0-1/ase/ga/relax_attaches.py 3.26.0-1/ase/ga/relax_attaches.py
--- 3.24.0-1/ase/ga/relax_attaches.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/relax_attaches.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ An object which can be associated with a local relaxation in order
 to make the relaxations run more smoothly."""
 from math import sqrt
diff -pruN 3.24.0-1/ase/ga/slab_operators.py 3.26.0-1/ase/ga/slab_operators.py
--- 3.24.0-1/ase/ga/slab_operators.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/slab_operators.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Operators that work on slabs.
 Allowed compositions are respected.
 Identical indexing of the slabs are assumed for the cut-splice operator."""
diff -pruN 3.24.0-1/ase/ga/soft_mutation.py 3.26.0-1/ase/ga/soft_mutation.py
--- 3.24.0-1/ase/ga/soft_mutation.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/soft_mutation.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Soft-mutation operator and associated tools"""
 import inspect
 import json
diff -pruN 3.24.0-1/ase/ga/standard_comparators.py 3.26.0-1/ase/ga/standard_comparators.py
--- 3.24.0-1/ase/ga/standard_comparators.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/standard_comparators.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.ga import get_raw_score
diff -pruN 3.24.0-1/ase/ga/standardmutations.py 3.26.0-1/ase/ga/standardmutations.py
--- 3.24.0-1/ase/ga/standardmutations.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/standardmutations.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """A collection of mutations that can be used."""
 from math import cos, pi, sin
 
diff -pruN 3.24.0-1/ase/ga/startgenerator.py 3.26.0-1/ase/ga/startgenerator.py
--- 3.24.0-1/ase/ga/startgenerator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/startgenerator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Tools for generating new random starting candidates."""
 import numpy as np
 
diff -pruN 3.24.0-1/ase/ga/utilities.py 3.26.0-1/ase/ga/utilities.py
--- 3.24.0-1/ase/ga/utilities.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/ga/utilities.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Various utility methods used troughout the GA."""
 import itertools
 import os
diff -pruN 3.24.0-1/ase/geometry/__init__.py 3.26.0-1/ase/geometry/__init__.py
--- 3.24.0-1/ase/geometry/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.cell import Cell
 from ase.geometry.cell import (
     cell_to_cellpar,
diff -pruN 3.24.0-1/ase/geometry/analysis.py 3.26.0-1/ase/geometry/analysis.py
--- 3.24.0-1/ase/geometry/analysis.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/analysis.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """Tools for analyzing instances of :class:`~ase.Atoms`
 """
diff -pruN 3.24.0-1/ase/geometry/bravais_type_engine.py 3.26.0-1/ase/geometry/bravais_type_engine.py
--- 3.24.0-1/ase/geometry/bravais_type_engine.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/bravais_type_engine.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import itertools
 
 import numpy as np
diff -pruN 3.24.0-1/ase/geometry/cell.py 3.26.0-1/ase/geometry/cell.py
--- 3.24.0-1/ase/geometry/cell.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/cell.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Copyright (C) 2010, Jesper Friis
 # (see accompanying license files for details).
 
diff -pruN 3.24.0-1/ase/geometry/dimensionality/disjoint_set.py 3.26.0-1/ase/geometry/dimensionality/disjoint_set.py
--- 3.24.0-1/ase/geometry/dimensionality/disjoint_set.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/dimensionality/disjoint_set.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 
diff -pruN 3.24.0-1/ase/geometry/dimensionality/interval_analysis.py 3.26.0-1/ase/geometry/dimensionality/interval_analysis.py
--- 3.24.0-1/ase/geometry/dimensionality/interval_analysis.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/dimensionality/interval_analysis.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Implements the dimensionality scoring parameter.
 
 Method is described in:
diff -pruN 3.24.0-1/ase/geometry/dimensionality/isolation.py 3.26.0-1/ase/geometry/dimensionality/isolation.py
--- 3.24.0-1/ase/geometry/dimensionality/isolation.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/dimensionality/isolation.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Implements functions for extracting ('isolating') a low-dimensional material
 component in its own unit cell.
diff -pruN 3.24.0-1/ase/geometry/dimensionality/rank_determination.py 3.26.0-1/ase/geometry/dimensionality/rank_determination.py
--- 3.24.0-1/ase/geometry/dimensionality/rank_determination.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/dimensionality/rank_determination.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Implements the Rank Determination Algorithm (RDA)
 
diff -pruN 3.24.0-1/ase/geometry/dimensionality/topology_scaling.py 3.26.0-1/ase/geometry/dimensionality/topology_scaling.py
--- 3.24.0-1/ase/geometry/dimensionality/topology_scaling.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/dimensionality/topology_scaling.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Implements the Topology-Scaling Algorithm (TSA)
 
 Method is described in:
diff -pruN 3.24.0-1/ase/geometry/distance.py 3.26.0-1/ase/geometry/distance.py
--- 3.24.0-1/ase/geometry/distance.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/distance.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 
diff -pruN 3.24.0-1/ase/geometry/geometry.py 3.26.0-1/ase/geometry/geometry.py
--- 3.24.0-1/ase/geometry/geometry.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/geometry.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Copyright (C) 2010, Jesper Friis
 # (see accompanying license files for details).
 
@@ -428,29 +430,12 @@ def get_duplicate_atoms(atoms, cutoff=0.
     Identify all atoms which lie within the cutoff radius of each other.
     Delete one set of them if delete == True.
     """
-    from scipy.spatial.distance import pdist
-    dists = pdist(atoms.get_positions(), 'sqeuclidean')
-    dup = np.nonzero(dists < cutoff**2)
-    rem = np.array(_row_col_from_pdist(len(atoms), dup[0]))
-    if delete:
-        if rem.size != 0:
-            del atoms[rem[:, 0]]
-    else:
-        return rem
-
-
-def _row_col_from_pdist(dim, i):
-    """Calculate the i,j index in the square matrix for an index in a
-    condensed (triangular) matrix.
-    """
-    i = np.array(i)
-    b = 1 - 2 * dim
-    x = (np.floor((-b - np.sqrt(b**2 - 8 * i)) / 2)).astype(int)
-    y = (i + x * (b + x + 2) / 2 + 1).astype(int)
-    if i.shape:
-        return list(zip(x, y))
-    else:
-        return [(x, y)]
+    dists = get_distances(atoms.positions, cell=atoms.cell, pbc=atoms.pbc)[1]
+    dup = np.argwhere(dists < cutoff)
+    dup = dup[dup[:, 0] < dup[:, 1]]  # indices at upper triangle
+    if delete and dup.size != 0:
+        del atoms[dup[:, 0]]
+    return dup
 
 
 def permute_axes(atoms, permutation):
diff -pruN 3.24.0-1/ase/geometry/minkowski_reduction.py 3.26.0-1/ase/geometry/minkowski_reduction.py
--- 3.24.0-1/ase/geometry/minkowski_reduction.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/minkowski_reduction.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import itertools
 
 import numpy as np
diff -pruN 3.24.0-1/ase/geometry/rdf.py 3.26.0-1/ase/geometry/rdf.py
--- 3.24.0-1/ase/geometry/rdf.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/geometry/rdf.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import math
 from typing import List, Optional, Tuple, Union
 
diff -pruN 3.24.0-1/ase/gui/add.py 3.26.0-1/ase/gui/add.py
--- 3.24.0-1/ase/gui/add.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/add.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 
 import numpy as np
@@ -13,7 +15,7 @@ current_selection_string = _('(selection
 class AddAtoms:
     def __init__(self, gui):
         self.gui = gui
-        win = self.win = ui.Window(_('Add atoms'), wmtype='utility')
+        win = self.win = ui.Window(_('Add atoms'))
         win.add(_('Specify chemical symbol, formula, or filename.'))
 
         def choose_file():
@@ -43,7 +45,7 @@ class AddAtoms:
         combobox = ui.ComboBox(labels, values)
         win.add([_('Add:'), combobox,
                  ui.Button(_('File ...'), callback=choose_file)])
-        combobox.widget.bind('<Return>', lambda e: self.add())
+        ui.bind_enter(combobox.widget, lambda e: self.add())
 
         combobox.value = default
         self.combobox = combobox
diff -pruN 3.24.0-1/ase/gui/ag.py 3.26.0-1/ase/gui/ag.py
--- 3.24.0-1/ase/gui/ag.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/ag.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Copyright 2008, 2009
 # CAMd (see accompanying license files for details).
 import warnings
@@ -7,7 +9,7 @@ class CLICommand:
     """ASE's graphical user interface.
 
     ASE-GUI.  See the online manual
-    (https://wiki.fysik.dtu.dk/ase/ase/gui/gui.html)
+    (https://ase-lib.org/ase/gui/gui.html)
     for more information.
     """
 
@@ -40,7 +42,7 @@ class CLICommand:
             help='Plot x,y1,y2,... graph from configurations or '
             'write data to sdtout in terminal mode.  Use the '
             'symbols: i, s, d, fmax, e, ekin, A, R, E and F.  See '
-            'https://wiki.fysik.dtu.dk/ase/ase/gui/gui.html'
+            'https://ase-lib.org/ase/gui/gui.html'
             '#plotting-data for more details.')
         add('-t', '--terminal',
             action='store_true',
diff -pruN 3.24.0-1/ase/gui/atomseditor.py 3.26.0-1/ase/gui/atomseditor.py
--- 3.24.0-1/ase/gui/atomseditor.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/gui/atomseditor.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,222 @@
+from dataclasses import dataclass
+from typing import Callable
+
+import numpy as np
+
+import ase.gui.ui as ui
+from ase.gui.i18n import _
+
+
+@dataclass
+class Column:
+    name: str
+    displayname: str
+    widget_width: int
+    getvalue: Callable
+    setvalue: Callable
+    format_value: Callable = lambda obj: str(obj)
+
+
+class AtomsEditor:
+    # We subscribe to gui.draw() calls in order to track changes,
+    # but we should have an actual "atoms changed" event instead.
+
+    def __init__(self, gui):
+        gui.obs.change_atoms.register(self.update_table_from_atoms)
+
+        win = ui.Window(_('Edit atoms'))
+
+        treeview = ui.ttk.Treeview(win.win, selectmode='extended')
+        edit_entry = ui.ttk.Entry(win.win)
+        edit_entry.pack(side='bottom', fill='x')
+        treeview.pack(side='left', fill='y')
+        bar = ui.ttk.Scrollbar(
+            win.win, orient='vertical', command=self.scroll_via_scrollbar
+        )
+        treeview.configure(yscrollcommand=self.scroll_via_treeview)
+
+        treeview.column('#0', width=40)
+        treeview.heading('#0', text=_('id'))
+
+        bar.pack(side='right', fill='y')
+        self.scrollbar = bar
+
+        def get_symbol(atoms, i):
+            return atoms.symbols[i]
+
+        def set_symbol(atoms, i, value):
+            from ase.data import atomic_numbers
+
+            if value not in atomic_numbers:
+                return  # Display error?
+            atoms.symbols[i] = value
+
+        self.gui = gui
+        self.treeview = treeview
+        self._current_entry = None
+
+        columns = []
+        symbols_column = Column(
+            'symbol', _('symbol'), 60, get_symbol, set_symbol
+        )
+        columns.append(symbols_column)
+
+        class GetSetPos:
+            def __init__(self, c):
+                self.c = c
+
+            def set_position(self, atoms, i, value):
+                try:
+                    value = float(value)
+                except ValueError:
+                    return
+                atoms.positions[i, self.c] = value
+
+            def get_position(self, atoms, i):
+                return atoms.positions[i, self.c]
+
+        for c, axisname in enumerate('xyz'):
+            column = Column(
+                axisname,
+                axisname,
+                92,
+                GetSetPos(c).get_position,
+                GetSetPos(c).set_position,
+                format_value=lambda val: f'{val:.4f}',
+            )
+            columns.append(column)
+
+        self.columns = columns
+
+        treeview.bind('<Double-1>', self.doubleclick)
+        treeview.bind('<<TreeviewSelect>>', self.treeview_selection_changed)
+
+        self.define_columns_on_widget()
+        self.update_table_from_atoms()
+
+        self.edit_entry = edit_entry
+
+    def treeview_selection_changed(self, event):
+        selected_items = self.treeview.selection()
+        indices = [self.rownumber(item) for item in selected_items]
+        self.gui.set_selected_atoms(indices)
+
+    def scroll_via_scrollbar(self, *args, **kwargs):
+        self.leave_edit_mode()
+        return self.treeview.yview(*args, **kwargs)
+
+    def scroll_via_treeview(self, *args, **kwargs):
+        # Here it is important to leave edit mode since scrolling
+        # invalidates the widget location.  Alternatively we could keep
+        # it open as long as we move it but that sounds like work
+        self.leave_edit_mode()
+        return self.scrollbar.set(*args, **kwargs)
+
+    def leave_edit_mode(self):
+        if self._current_entry is not None:
+            self._current_entry.destroy()
+            self._current_entry = None
+            self.treeview.focus_force()
+
+    @property
+    def atoms(self):
+        return self.gui.atoms
+
+    def update_table_from_atoms(self):
+        self.treeview.delete(*self.treeview.get_children())
+        for i in range(len(self.atoms)):
+            values = self.get_row_values(i)
+            self.treeview.insert(
+                '', 'end', text=i, values=values, iid=self.rowid(i)
+            )
+
+        mask = self.gui.images.selected[: len(self.atoms)]
+        selection = np.arange(len(self.atoms))[mask]
+
+        rowids = [self.rowid(index) for index in selection]
+        # Note: selection_set() does *not* fire an event, and therefore
+        # we do not need to worry about infinite recursion.
+        # However the event listening is wonky now because we need
+        # better GUI change listeners.
+        self.treeview.selection_set(*rowids)
+
+    def get_row_values(self, i):
+        return [
+            column.format_value(column.getvalue(self.atoms, i))
+            for column in self.columns
+        ]
+
+    def define_columns_on_widget(self):
+        self.treeview['columns'] = [column.name for column in self.columns]
+        for column in self.columns:
+            self.treeview.heading(column.name, text=column.displayname)
+            self.treeview.column(
+                column.name,
+                width=column.widget_width,
+                anchor='e',
+            )
+
+    def rowid(self, rownumber: int) -> str:
+        return f'R{rownumber}'
+
+    def rownumber(self, rowid: str) -> int:
+        assert rowid.startswith('R'), repr(rowid)
+        return int(rowid[1:])
+
+    def set_value(self, column_no: int, row_no: int, value: object) -> None:
+        column = self.columns[column_no]
+        column.setvalue(self.atoms, row_no, value)
+        text = column.format_value(column.getvalue(self.atoms, row_no))
+
+        # The text that we set here is not what matters: It may be rounded.
+        # It was column.setvalue() which did the actual change.
+        self.treeview.set(self.rowid(row_no), column.name, value=text)
+
+        # (Maybe it is not always necessary to redraw everything.)
+        self.gui.set_frame()
+
+    def doubleclick(self, event):
+        row_id = self.treeview.identify_row(event.y)
+        column_id = self.treeview.identify_column(event.x)
+        if not row_id or not column_id:
+            return  # clicked outside actual rows/columns
+        self.edit_field(row_id, column_id)
+
+    def edit_field(self, row_id, column_id):
+        assert column_id.startswith('#'), repr(column_id)
+        column_no = int(column_id[1:]) - 1
+
+        if column_no == -1:
+            return  # This is the ID column.
+
+        row_no = self.rownumber(row_id)
+        assert 0 <= column_no < len(self.columns)
+        assert 0 <= row_no < len(self.atoms)
+
+        content = self.columns[column_no].getvalue(self.atoms, row_no)
+
+        assert self._current_entry is None
+        entry = ui.ttk.Entry(self.treeview)
+        entry.insert(0, content)
+        entry.focus_force()
+        entry.selection_range(0, 'end')
+
+        def apply_change(_event=None):
+            value = entry.get()
+            try:
+                self.set_value(column_no, row_no, value)
+            finally:
+                # Focus was given to the text field, now return it:
+                self.treeview.focus_force()
+                self.leave_edit_mode()
+
+        entry.bind('<FocusOut>', apply_change)
+        ui.bind_enter(entry, apply_change)
+        entry.bind('<Escape>', lambda *args: self.leave_edit_mode())
+
+        bbox = self.treeview.bbox(row_id, column_id)
+        if bbox:  # (bbox is '' when testing without display)
+            x, y, width, height = bbox
+            entry.place(x=x, y=y, height=height)
+        self._current_entry = entry
+        return entry, apply_change
diff -pruN 3.24.0-1/ase/gui/celleditor.py 3.26.0-1/ase/gui/celleditor.py
--- 3.24.0-1/ase/gui/celleditor.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/celleditor.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 '''celleditor.py - Window for editing the cell of an atoms object
 '''
 import numpy as np
@@ -12,7 +14,7 @@ class CellEditor:
 
     def __init__(self, gui):
         self.gui = gui
-        self.gui.register_vulnerable(self)
+        self.gui.obs.set_atoms.register(self.notify_atoms_changed)
 
         # Create grid control for cells
         # xx xy xz ||x|| pbc
@@ -46,7 +48,7 @@ class CellEditor:
         self.vacuum = ui.SpinBox(5, 0, 15, 0.1, self.apply_vacuum)
 
         # TRANSLATORS: This is a title of a window.
-        win = self.win = ui.Window(_('Cell Editor'), wmtype='utility')
+        win = self.win = ui.Window(_('Cell Editor'))
 
         x, y, z = self.cell_grid
 
diff -pruN 3.24.0-1/ase/gui/colors.py 3.26.0-1/ase/gui/colors.py
--- 3.24.0-1/ase/gui/colors.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/colors.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """colors.py - select how to color the atoms in the GUI."""
 import numpy as np
 
@@ -14,7 +16,7 @@ class ColorWindow:
 
     def reset(self, gui):
         """create a new color window"""
-        self.win = ui.Window(_('Colors'), wmtype='utility')
+        self.win = ui.Window(_('Colors'))
         self.gui = gui
         self.win.add(ui.Label(_('Choose how the atoms are colored:')))
         values = ['jmol', 'tag', 'force', 'velocity',
diff -pruN 3.24.0-1/ase/gui/constraints.py 3.26.0-1/ase/gui/constraints.py
--- 3.24.0-1/ase/gui/constraints.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/constraints.py	2025-08-12 11:26:23.000000000 +0000
@@ -4,11 +4,11 @@ from ase.gui.i18n import _
 
 class Constraints:
     def __init__(self, gui):
-        win = ui.Window(_('Constraints'), wmtype='utility')
-        win.add([ui.Button(_('Fix'), self.selected),
-                 _('selected atoms')])
-        win.add([ui.Button(_('Release'), self.unconstrain),
-                 _('selected atoms')])
+        win = ui.Window(_('Constraints'))
+        win.add([ui.Button(_('Fix'), self.selected), _('selected atoms')])
+        win.add(
+            [ui.Button(_('Release'), self.unconstrain), _('selected atoms')]
+        )
         win.add(ui.Button(_('Clear all constraints'), self.clear))
         self.gui = gui
 
diff -pruN 3.24.0-1/ase/gui/defaults.py 3.26.0-1/ase/gui/defaults.py
--- 3.24.0-1/ase/gui/defaults.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/defaults.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This is a module to handle generic ASE (gui) defaults ...
 
 ... from a ~/.ase/gui.py configuration file, if it exists. It is imported when
@@ -16,6 +18,7 @@ gui_default_settings = {
     'radii_scale': 0.89,
     'force_vector_scale': 1.0,
     'velocity_vector_scale': 1.0,
+    'magmom_vector_scale': 1.0,
     'show_unit_cell': True,
     'show_axes': True,
     'show_bonds': False,
diff -pruN 3.24.0-1/ase/gui/graphs.py 3.26.0-1/ase/gui/graphs.py
--- 3.24.0-1/ase/gui/graphs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/graphs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import pickle
 import sys
 
@@ -33,7 +35,7 @@ Symbols:
 
 class Graphs:
     def __init__(self, gui):
-        win = ui.Window('Graphs', wmtype='utility')
+        win = ui.Window('Graphs')
         self.expr = ui.Entry('', 50, self.plot)
         win.add([self.expr, ui.helpbutton(graph_help_text)])
 
@@ -65,12 +67,6 @@ class Graphs:
     def save(self):
         dialog = ui.SaveFileDialog(self.gui.window.win,
                                    _('Save data to file ... '))
-        # fix tkinter not automatically setting dialog type
-        # remove from Python3.8+
-        # see https://github.com/python/cpython/pull/25187
-        # and https://bugs.python.org/issue43655
-        # and https://github.com/python/cpython/pull/25592
-        ui.set_windowtype(dialog.top, 'dialog')
         filename = dialog.go()
         if filename:
             expr = self.expr.value
diff -pruN 3.24.0-1/ase/gui/gui.py 3.26.0-1/ase/gui/gui.py
--- 3.24.0-1/ase/gui/gui.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/gui.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,7 +1,8 @@
+# fmt: off
+
 import pickle
 import subprocess
 import sys
-import weakref
 from functools import partial
 from time import time
 
@@ -14,6 +15,7 @@ from ase.gui.i18n import _
 from ase.gui.images import Images
 from ase.gui.nanoparticle import SetupNanoparticle
 from ase.gui.nanotube import SetupNanotube
+from ase.gui.observer import Observers
 from ase.gui.save import save_dialog
 from ase.gui.settings import Settings
 from ase.gui.status import Status
@@ -21,7 +23,14 @@ from ase.gui.surfaceslab import SetupSur
 from ase.gui.view import View
 
 
-class GUI(View, Status):
+class GUIObservers:
+    def __init__(self):
+        self.new_atoms = Observers()
+        self.set_atoms = Observers()
+        self.change_atoms = Observers()
+
+
+class GUI(View):
     ARROWKEY_SCAN = 0
     ARROWKEY_MOVE = 1
     ARROWKEY_ROTATE = 2
@@ -34,7 +43,10 @@ class GUI(View, Status):
             images = Images(images)
 
         self.images = images
+
+        # Ordinary observers seem unused now, delete?
         self.observers = []
+        self.obs = GUIObservers()
 
         self.config = read_defaults()
         if show_bonds:
@@ -49,12 +61,11 @@ class GUI(View, Status):
                                       release=self.release,
                                       resize=self.resize)
 
-        View.__init__(self, rotations)
-        Status.__init__(self)
+        super().__init__(rotations)
+        self.status = Status(self)
 
         self.subprocesses = []  # list of external processes
         self.movie_window = None
-        self.vulnerable_windows = []
         self.simulation = {}  # Used by modules on Calculate menu.
         self.module_state = {}  # Used by modules to store their state.
 
@@ -146,13 +157,16 @@ class GUI(View, Status):
         return Settings(self)
 
     def scroll(self, event):
-        CTRL = event.modifier == 'ctrl'
+        shift = 0x1
+        ctrl = 0x4
+        alt_l = 0x8  # Also Mac Command Key
+        mac_option_key = 0x10
 
-        # Bug: Simultaneous CTRL + shift is the same as just CTRL.
-        # Therefore movement in Z direction does not support the
-        # shift modifier.
-        dxdydz = {'up': (0, 1 - CTRL, CTRL),
-                  'down': (0, -1 + CTRL, -CTRL),
+        use_small_step = bool(event.state & shift)
+        rotate_into_plane = bool(event.state & (ctrl | alt_l | mac_option_key))
+
+        dxdydz = {'up': (0, 1 - rotate_into_plane, rotate_into_plane),
+                  'down': (0, -1 + rotate_into_plane, -rotate_into_plane),
                   'right': (1, 0, 0),
                   'left': (-1, 0, 0)}.get(event.key, None)
 
@@ -175,7 +189,7 @@ class GUI(View, Status):
             return
 
         vec = 0.1 * np.dot(self.axes, dxdydz)
-        if event.modifier == 'shift':
+        if use_small_step:
             vec *= 0.1
 
         if self.arrowkey_mode == self.ARROWKEY_MOVE:
@@ -222,6 +236,18 @@ class GUI(View, Status):
         from ase.gui.constraints import Constraints
         return Constraints(self)
 
+    def set_selected_atoms(self, selected):
+        newmask = np.zeros(len(self.images.selected), bool)
+        newmask[selected] = True
+
+        if np.array_equal(newmask, self.images.selected):
+            return
+
+        # (By creating newmask, we can avoid resetting the selection in
+        # case the selected indices are invalid)
+        self.images.selected[:] = newmask
+        self.draw()
+
     def select_all(self, key=None):
         self.images.selected[:] = True
         self.draw()
@@ -329,9 +355,13 @@ class GUI(View, Status):
         from ase.gui.celleditor import CellEditor
         return CellEditor(self)
 
+    def atoms_editor(self, key=None):
+        from ase.gui.atomseditor import AtomsEditor
+        return AtomsEditor(self)
+
     def quick_info_window(self, key=None):
         from ase.gui.quickinfo import info
-        info_win = ui.Window(_('Quick Info'), wmtype='utility')
+        info_win = ui.Window(_('Quick Info'))
         info_win.add(info(self))
 
         # Update quickinfo window when we change frame
@@ -361,30 +391,7 @@ class GUI(View, Status):
         self.frame = 0  # Prevent crashes
         self.images.repeat_images(rpt)
         self.set_frame(frame=0, focus=True)
-        self.notify_vulnerable()
-
-    def notify_vulnerable(self):
-        """Notify windows that would break when new_atoms is called.
-
-        The notified windows may adapt to the new atoms.  If that is not
-        possible, they should delete themselves.
-        """
-        new_vul = []  # Keep weakrefs to objects that still exist.
-        for wref in self.vulnerable_windows:
-            ref = wref()
-            if ref is not None:
-                new_vul.append(wref)
-                ref.notify_atoms_changed()
-        self.vulnerable_windows = new_vul
-
-    def register_vulnerable(self, obj):
-        """Register windows that are vulnerable to changing the images.
-
-        Some windows will break if the atoms (and in particular the
-        number of images) are changed.  They can register themselves
-        and be closed when that happens.
-        """
-        self.vulnerable_windows.append(weakref.ref(obj))
+        self.obs.new_atoms.notify()
 
     def exit(self, event=None):
         for process in self.subprocesses:
@@ -405,6 +412,12 @@ class GUI(View, Status):
         selection_mask = self.images.selected[:len(self.atoms)]
         return self.atoms[selection_mask]
 
+    def wrap_atoms(self, key=None):
+        """Wrap atoms around the unit cell."""
+        for atoms in self.images:
+            atoms.wrap()
+        self.set_frame()
+
     @property
     def clipboard(self):
         from ase.gui.clipboard import AtomsClipboard
@@ -492,7 +505,8 @@ class GUI(View, Status):
               M(_('_Add atoms'), self.add_atoms, 'Ctrl+A'),
               M(_('_Delete selected atoms'), self.delete_selected_atoms,
                 'Backspace'),
-              M(_('Edit _cell'), self.cell_editor, 'Ctrl+E'),
+              M(_('Edit _cell …'), self.cell_editor, 'Ctrl+E'),
+              M(_('Edit _atoms …'), self.atoms_editor, 'A'),
               M('---'),
               M(_('_First image'), self.step, 'Home'),
               M(_('_Previous image'), self.step, 'Page-Up'),
@@ -511,6 +525,8 @@ class GUI(View, Status):
                 value=False),
               M(_('Show _forces'), self.toggle_show_forces, 'Ctrl+F',
                 value=False),
+              M(_('Show _magmoms'), self.toggle_show_magmoms,
+                value=False),
               M(_('Show _Labels'), self.show_labels,
                 choices=[_('_None'),
                          _('Atom _Index'),
@@ -533,15 +549,15 @@ class GUI(View, Status):
                     M(_('xy-plane'), self.set_view, 'Z'),
                     M(_('yz-plane'), self.set_view, 'X'),
                     M(_('zx-plane'), self.set_view, 'Y'),
-                    M(_('yx-plane'), self.set_view, 'Alt+Z'),
-                    M(_('zy-plane'), self.set_view, 'Alt+X'),
-                    M(_('xz-plane'), self.set_view, 'Alt+Y'),
-                    M(_('a2,a3-plane'), self.set_view, '1'),
-                    M(_('a3,a1-plane'), self.set_view, '2'),
-                    M(_('a1,a2-plane'), self.set_view, '3'),
-                    M(_('a3,a2-plane'), self.set_view, 'Alt+1'),
-                    M(_('a1,a3-plane'), self.set_view, 'Alt+2'),
-                    M(_('a2,a1-plane'), self.set_view, 'Alt+3')]),
+                    M(_('yx-plane'), self.set_view, 'Shift+Z'),
+                    M(_('zy-plane'), self.set_view, 'Shift+X'),
+                    M(_('xz-plane'), self.set_view, 'Shift+Y'),
+                    M(_('a2,a3-plane'), self.set_view, 'I'),
+                    M(_('a3,a1-plane'), self.set_view, 'J'),
+                    M(_('a1,a2-plane'), self.set_view, 'K'),
+                    M(_('a3,a2-plane'), self.set_view, 'Shift+I'),
+                    M(_('a1,a3-plane'), self.set_view, 'Shift+J'),
+                    M(_('a2,a1-plane'), self.set_view, 'Shift+K')]),
               M(_('Settings ...'), self.settings),
               M('---'),
               M(_('VMD'), partial(self.external_viewer, 'vmd')),
@@ -559,7 +575,8 @@ class GUI(View, Status):
                 'Ctrl+R'),
               M(_('NE_B plot'), self.neb),
               M(_('B_ulk Modulus'), self.bulk_modulus),
-              M(_('Reciprocal space ...'), self.reciprocal)]),
+              M(_('Reciprocal space ...'), self.reciprocal),
+              M(_('Wrap atoms'), self.wrap_atoms, 'Ctrl+W')]),
 
             # TRANSLATORS: Set up (i.e. build) surfaces, nanoparticles, ...
             (_('_Setup'),
@@ -575,10 +592,10 @@ class GUI(View, Status):
             #    disabled=True)]),
 
             (_('_Help'),
-             [M(_('_About'), partial(ui.about, 'ASE-GUI',
-                                     version=__version__,
-                                     webpage='https://wiki.fysik.dtu.dk/'
-                                     'ase/ase/gui/gui.html')),
+             [M(_('_About'), partial(
+                 ui.about, 'ASE-GUI',
+                 version=__version__,
+                 webpage='https://ase-lib.org/ase/gui/gui.html')),
               M(_('Webpage ...'), webpage)])]
 
     def attach(self, function, *args, **kwargs):
@@ -636,4 +653,4 @@ class GUI(View, Status):
 
 def webpage():
     import webbrowser
-    webbrowser.open('https://wiki.fysik.dtu.dk/ase/ase/gui/gui.html')
+    webbrowser.open('https://ase-lib.org/ase/gui/gui.html')
diff -pruN 3.24.0-1/ase/gui/images.py 3.26.0-1/ase/gui/images.py
--- 3.24.0-1/ase/gui/images.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/images.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import warnings
 from math import sqrt
 
@@ -85,9 +87,6 @@ class Images:
             self._images.append(atoms)
             self.have_varying_species |= not np.array_equal(self[0].numbers,
                                                             atoms.numbers)
-            if hasattr(self, 'Q'):
-                assert False  # XXX askhl fix quaternions
-                self.Q[i] = atoms.get_quaternions()
             if (atoms.pbc != self[0].pbc).any():
                 warning = True
 
diff -pruN 3.24.0-1/ase/gui/modify.py 3.26.0-1/ase/gui/modify.py
--- 3.24.0-1/ase/gui/modify.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/modify.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from functools import partial
 
 import numpy as np
@@ -20,7 +22,7 @@ class ModifyAtoms:
             ui.error(_('No atoms selected!'))
             return
 
-        win = ui.Window(_('Modify'), wmtype='utility')
+        win = ui.Window(_('Modify'))
         element = Element(callback=self.set_element)
         win.add(element)
         win.add(ui.Button(_('Change element'),
diff -pruN 3.24.0-1/ase/gui/movie.py 3.26.0-1/ase/gui/movie.py
--- 3.24.0-1/ase/gui/movie.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/movie.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 import ase.gui.ui as ui
@@ -6,8 +8,7 @@ from ase.gui.i18n import _
 
 class Movie:
     def __init__(self, gui):
-        self.win = win = ui.Window(
-            _('Movie'), close=self.close, wmtype='utility')
+        self.win = win = ui.Window(_('Movie'), close=self.close)
         win.add(_('Image number:'))
         self.frame_number = ui.Scale(gui.frame, 0,
                                      len(gui.images) - 1,
@@ -43,11 +44,7 @@ class Movie:
         self.gui = gui
         self.direction = 1
         self.timer = None
-        gui.register_vulnerable(self)
-
-    def notify_atoms_changed(self):
-        """Called by gui object when the atoms have changed."""
-        self.close()
+        gui.obs.new_atoms.register(self.close)
 
     def close(self):
         self.stop()
diff -pruN 3.24.0-1/ase/gui/nanoparticle.py 3.26.0-1/ase/gui/nanoparticle.py
--- 3.24.0-1/ase/gui/nanoparticle.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/nanoparticle.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """nanoparticle.py - Window for setting up crystalline nanoparticles.
 """
 from copy import copy
@@ -133,7 +135,7 @@ class SetupNanoparticle:
         self.no_update = True
         self.old_structure = 'fcc'
 
-        win = self.win = ui.Window(_('Nanoparticle'), wmtype='utility')
+        win = self.win = ui.Window(_('Nanoparticle'))
         win.add(ui.Text(introtext))
 
         self.element = Element('', self.apply)
diff -pruN 3.24.0-1/ase/gui/nanotube.py 3.26.0-1/ase/gui/nanotube.py
--- 3.24.0-1/ase/gui/nanotube.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/nanotube.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Window for setting up Carbon nanotubes and similar tubes.
 """
 
@@ -35,7 +37,7 @@ class SetupNanotube:
         self.length = ui.SpinBox(1, 1, 100, 1, self.make)
         self.description = ui.Label('')
 
-        win = self.win = ui.Window(_('Nanotube'), wmtype='utility')
+        win = self.win = ui.Window(_('Nanotube'))
         win.add(ui.Text(introtext))
         win.add(self.element)
         win.add([_('Bond length: '),
diff -pruN 3.24.0-1/ase/gui/observer.py 3.26.0-1/ase/gui/observer.py
--- 3.24.0-1/ase/gui/observer.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/gui/observer.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,44 @@
+import warnings
+import weakref
+
+
+class Observers:
+    def __init__(self):
+        self.observer_weakrefs = []
+
+    def register(self, observer):
+        if hasattr(observer, '__self__'):  # observer is an instance method
+            # Since bound methods are shortlived we need to store the instance
+            # it is bound to and use getattr() later:
+            obj = observer.__self__
+            name = observer.__name__
+        else:
+            obj = observer
+            name = None
+        self.observer_weakrefs.append((weakref.ref(obj), name))
+
+    def notify(self):
+        # We should probably add an event class to these callbacks.
+        weakrefs_still_alive = []
+        for weak_ref, name in self.observer_weakrefs:
+            observer = weak_ref()
+            if observer is not None:
+                weakrefs_still_alive.append((weak_ref, name))
+                if name is not None:
+                    # If the observer is an instance method we stored
+                    # self, for garbage collection reasons, and now need to
+                    # get the actual method:
+                    observer = getattr(observer, name)
+
+                try:
+                    observer()
+                except Exception as ex:
+                    import traceback
+
+                    tb = ''.join(traceback.format_exception(ex))
+                    warnings.warn(
+                        f'Suppressed exception in observer {observer}: {tb}'
+                    )
+                    continue
+
+        self.observer_weakrefs = weakrefs_still_alive
diff -pruN 3.24.0-1/ase/gui/pipe.py 3.26.0-1/ase/gui/pipe.py
--- 3.24.0-1/ase/gui/pipe.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/pipe.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import pickle
 import sys
 
diff -pruN 3.24.0-1/ase/gui/po/da/LC_MESSAGES/ag.po 3.26.0-1/ase/gui/po/da/LC_MESSAGES/ag.po
--- 3.24.0-1/ase/gui/po/da/LC_MESSAGES/ag.po	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/po/da/LC_MESSAGES/ag.po	2025-08-12 11:26:23.000000000 +0000
@@ -3435,12 +3435,12 @@ msgstr "Ingen Pythonkode"
 #~ msgid ""
 #~ "Plot x,y1,y2,... graph from configurations or write data to sdtout in "
 #~ "terminal mode.  Use the symbols: i, s, d, fmax, e, ekin, A, R, E and F.  "
-#~ "See https://wiki.fysik.dtu.dk/ase/ase/gui.html#plotting-data for more "
+#~ "See https://ase-lib.org/ase/gui.html#plotting-data for more "
 #~ "details."
 #~ msgstr ""
 #~ "Tegn graf for x,y1,y2,... fra konfigurationer, eller skriv data til "
 #~ "stdout i teksttilstand.  Brug symbolerne i, s, d, fmax, e, ekin, A, R, E "
-#~ "og F.  Yderligere detaljer kan findes på https://wiki.fysik.dtu.dk/ase/"
+#~ "og F.  Yderligere detaljer kan findes på https://ase-lib.org/"
 #~ "ase/gui.html#plotting-data for more details."
 
 #~ msgid "Run in terminal window - no GUI."
diff -pruN 3.24.0-1/ase/gui/po/en_GB/LC_MESSAGES/ag.po 3.26.0-1/ase/gui/po/en_GB/LC_MESSAGES/ag.po
--- 3.24.0-1/ase/gui/po/en_GB/LC_MESSAGES/ag.po	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/po/en_GB/LC_MESSAGES/ag.po	2025-08-12 11:26:23.000000000 +0000
@@ -3441,12 +3441,12 @@ msgstr "No Python code"
 #~ msgid ""
 #~ "Plot x,y1,y2,... graph from configurations or write data to sdtout in "
 #~ "terminal mode.  Use the symbols: i, s, d, fmax, e, ekin, A, R, E and F.  "
-#~ "See https://wiki.fysik.dtu.dk/ase/ase/gui.html#plotting-data for more "
+#~ "See https://ase-lib.org/ase/gui.html#plotting-data for more "
 #~ "details."
 #~ msgstr ""
 #~ "Plot x,y1,y2,... graph from configurations or write data to sdtout in "
 #~ "terminal mode.  Use the symbols: i, s, d, fmax, e, ekin, A, R, E and F.  "
-#~ "See https://wiki.fysik.dtu.dk/ase/ase/gui.html#plotting-data for more "
+#~ "See https://ase-lib.org/ase/gui.html#plotting-data for more "
 #~ "details."
 
 #~ msgid "Run in terminal window - no GUI."
diff -pruN 3.24.0-1/ase/gui/quickinfo.py 3.26.0-1/ase/gui/quickinfo.py
--- 3.24.0-1/ase/gui/quickinfo.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/quickinfo.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 "Module for displaying information about the system."
 
 
diff -pruN 3.24.0-1/ase/gui/render.py 3.26.0-1/ase/gui/render.py
--- 3.24.0-1/ase/gui/render.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/render.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from os import unlink
 
 import numpy as np
@@ -16,8 +18,7 @@ class Render:
 
     def __init__(self, gui):
         self.gui = gui
-        self.win = win = ui.Window(
-            _('Render current view in povray ... '), wmtype='utility')
+        self.win = win = ui.Window(_('Render current view in povray ... '))
         win.add(ui.Label(_("Rendering %d atoms.") % len(self.gui.atoms)))
 
         guiwidth, guiheight = self.get_guisize()
diff -pruN 3.24.0-1/ase/gui/repeat.py 3.26.0-1/ase/gui/repeat.py
--- 3.24.0-1/ase/gui/repeat.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/repeat.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,10 +1,12 @@
+# fmt: off
+
 import ase.gui.ui as ui
 from ase.gui.i18n import _
 
 
 class Repeat:
     def __init__(self, gui):
-        win = ui.Window(_('Repeat'), wmtype='utility')
+        win = ui.Window(_('Repeat'))
         win.add(_('Repeat atoms:'))
         self.repeat = [ui.SpinBox(r, 1, 9, 1, self.change)
                        for r in gui.images.repeat]
diff -pruN 3.24.0-1/ase/gui/rotate.py 3.26.0-1/ase/gui/rotate.py
--- 3.24.0-1/ase/gui/rotate.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/rotate.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import ase.gui.ui as ui
 from ase.gui.i18n import _
 from ase.utils import irotate, rotate
@@ -8,7 +10,7 @@ class Rotate:
 
     def __init__(self, gui):
         self.gui = gui
-        win = ui.Window(_('Rotate'), wmtype='utility')
+        win = ui.Window(_('Rotate'))
         win.add(_('Rotation angles:'))
         self.rotate = [ui.SpinBox(42.0, -360, 360, 1, self.change)
                        for _ in '123']
diff -pruN 3.24.0-1/ase/gui/save.py 3.26.0-1/ase/gui/save.py
--- 3.24.0-1/ase/gui/save.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/save.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Dialog for saving one or more configurations."""
 
 import numpy as np
@@ -25,12 +27,6 @@ last image. Examples: "name@-1": last im
 
 def save_dialog(gui, filename=None):
     dialog = ui.SaveFileDialog(gui.window.win, _('Save ...'))
-    # fix tkinter not automatically setting dialog type
-    # remove from Python3.8+
-    # see https://github.com/python/cpython/pull/25187
-    # and https://bugs.python.org/issue43655
-    # and https://github.com/python/cpython/pull/25592
-    ui.set_windowtype(dialog.top, 'dialog')
     ui.Text(text).pack(dialog.top)
     filename = filename or dialog.go()
     if not filename:
diff -pruN 3.24.0-1/ase/gui/settings.py 3.26.0-1/ase/gui/settings.py
--- 3.24.0-1/ase/gui/settings.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/settings.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import ase.gui.ui as ui
 from ase.gui.i18n import _
 
@@ -5,7 +7,7 @@ from ase.gui.i18n import _
 class Settings:
     def __init__(self, gui):
         self.gui = gui
-        win = ui.Window(_('Settings'), wmtype='utility')
+        win = ui.Window(_('Settings'))
 
         # Constraints
         win.add(_('Constraints:'))
@@ -42,6 +44,13 @@ class Settings:
             callback=self.scale_velocity_vectors
         )
         win.add([_('Scale velocity vectors:'), self.velocity_vector_scale])
+        self.magmom_vector_scale = ui.SpinBox(
+            self.gui.magmom_vector_scale,
+            0.0, 1e32, 0.1,
+            rounding=2,
+            callback=self.scale_magmom_vectors
+        )
+        win.add([_('Scale magmom vectors:'), self.magmom_vector_scale])
 
     def scale_radii(self):
         self.gui.images.atom_scale = self.scale.value
@@ -58,6 +67,11 @@ class Settings:
         self.gui.draw()
         return True
 
+    def scale_magmom_vectors(self):
+        self.gui.magmom_vector_scale = float(self.magmom_vector_scale.value)
+        self.gui.draw()
+        return True
+
     def hide_selected(self):
         self.gui.images.visible[self.gui.images.selected] = False
         self.gui.draw()
diff -pruN 3.24.0-1/ase/gui/status.py 3.26.0-1/ase/gui/status.py
--- 3.24.0-1/ase/gui/status.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/status.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import warnings
 from math import acos, pi, sqrt
 
@@ -25,15 +27,15 @@ def formula(Z):
     return '+'.join(strings)
 
 
-class Status:  # Status is used as a mixin in GUI
-    def __init__(self):
-        self.ordered_indices = []
+class Status:
+    def __init__(self, gui):
+        self.gui = gui
 
     def status(self, atoms):
-        # use where here:  XXX
+        gui = self.gui
         natoms = len(atoms)
-        indices = np.arange(natoms)[self.images.selected[:natoms]]
-        ordered_indices = [i for i in self.images.selected_ordered
+        indices = np.arange(natoms)[gui.images.selected[:natoms]]
+        ordered_indices = [i for i in gui.images.selected_ordered
                            if i < len(atoms)]
         n = len(indices)
 
@@ -67,7 +69,7 @@ class Status:  # Status is used as a mix
                 if forces is not None:
                     maxf = np.linalg.norm(forces, axis=1).max()
                     line += f'   Max force = {maxf:.3f} eV/Å'
-            self.window.update_status_line(line)
+            gui.window.update_status_line(line)
             return
 
         Z = atoms.numbers[indices]
@@ -78,12 +80,12 @@ class Status:  # Status is used as a mix
             text = (' #%d %s (%s): %.3f Å, %.3f Å, %.3f Å ' %
                     ((indices[0], names[Z[0]], symbols[Z[0]]) + tuple(R[0])))
             text += _(' tag=%(tag)s') % dict(tag=tag)
-            magmoms = get_magmoms(self.atoms)
+            magmoms = get_magmoms(gui.atoms)
             if magmoms.any():
                 # TRANSLATORS: mom refers to magnetic moment
                 text += _(' mom={:1.2f}'.format(
                     magmoms[indices][0]))
-            charges = self.atoms.get_initial_charges()
+            charges = gui.atoms.get_initial_charges()
             if charges.any():
                 text += _(' q={:1.2f}'.format(
                     charges[indices][0]))
@@ -121,10 +123,10 @@ class Status:  # Status is used as a mix
             text = (' %s-%s-%s: %.1f°, %.1f°, %.1f°' %
                     tuple([symbols[z] for z in Z] + a))
         elif len(ordered_indices) == 4:
-            angle = self.atoms.get_dihedral(*ordered_indices, mic=True)
+            angle = gui.atoms.get_dihedral(*ordered_indices, mic=True)
             text = ('%s %s → %s → %s → %s: %.1f°' %
                     tuple([_('dihedral')] + [symbols[z] for z in Z] + [angle]))
         else:
             text = ' ' + formula(Z)
 
-        self.window.update_status_line(text)
+        gui.window.update_status_line(text)
diff -pruN 3.24.0-1/ase/gui/surfaceslab.py 3.26.0-1/ase/gui/surfaceslab.py
--- 3.24.0-1/ase/gui/surfaceslab.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/surfaceslab.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 '''surfaceslab.py - Window for setting up surfaces
 '''
 import ase.build as build
@@ -62,7 +64,7 @@ class SetupSurfaceSlab:
         self.vacuum = ui.SpinBox(5, 0, 40, 0.01, self.make)
         self.description = ui.Label('')
 
-        win = self.win = ui.Window(_('Surface'), wmtype='utility')
+        win = self.win = ui.Window(_('Surface'))
         win.add(ui.Text(introtext))
         win.add(self.element)
         win.add([_('Structure:'), self.structure, self.structure_warn])
diff -pruN 3.24.0-1/ase/gui/ui.py 3.26.0-1/ase/gui/ui.py
--- 3.24.0-1/ase/gui/ui.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/ui.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,6 +1,8 @@
+# fmt: off
+
 # type: ignore
+import platform
 import re
-import sys
 import tkinter as tk
 import tkinter.ttk as ttk
 from collections import namedtuple
@@ -17,13 +19,7 @@ __all__ = [
     'error', 'ask_question', 'MainWindow', 'LoadFileDialog', 'SaveFileDialog',
     'ASEGUIWindow', 'Button', 'CheckButton', 'ComboBox', 'Entry', 'Label',
     'Window', 'MenuItem', 'RadioButton', 'RadioButtons', 'Rows', 'Scale',
-    'showinfo', 'showwarning', 'SpinBox', 'Text', 'set_windowtype']
-
-
-if sys.platform == 'darwin':
-    mouse_buttons = {2: 3, 3: 2}
-else:
-    mouse_buttons = {}
+    'showinfo', 'showwarning', 'SpinBox', 'Text']
 
 
 def error(title, message=None):
@@ -39,7 +35,6 @@ def about(name, version, webpage):
             _('Version') + ': ' + version,
             _('Web-page') + ': ' + webpage]
     win = Window(_('About'))
-    set_windowtype(win.win, 'dialog')
     win.add(Text('\n'.join(text)))
 
 
@@ -49,21 +44,11 @@ def helpbutton(text):
 
 def helpwindow(text):
     win = Window(_('Help'))
-    set_windowtype(win.win, 'dialog')
     win.add(Text(text))
 
 
-def set_windowtype(win, wmtype):
-    # only on X11
-    # WM_TYPE, for possible settings see
-    # https://specifications.freedesktop.org/wm-spec/wm-spec-latest.html#idm45623487848608
-    # you want dialog, normal or utility most likely
-    if win._windowingsystem == "x11":
-        win.wm_attributes('-type', wmtype)
-
-
 class BaseWindow:
-    def __init__(self, title, close=None, wmtype='normal'):
+    def __init__(self, title, close=None):
         self.title = title
         if close:
             self.win.protocol('WM_DELETE_WINDOW', close)
@@ -72,7 +57,6 @@ class BaseWindow:
 
         self.things = []
         self.exists = True
-        set_windowtype(self.win, wmtype)
 
     def close(self):
         self.win.destroy()
@@ -93,9 +77,9 @@ class BaseWindow:
 
 
 class Window(BaseWindow):
-    def __init__(self, title, close=None, wmtype='normal'):
+    def __init__(self, title, close=None):
         self.win = tk.Toplevel()
-        BaseWindow.__init__(self, title, close, wmtype)
+        super().__init__(title, close)
 
 
 class Widget:
@@ -218,7 +202,7 @@ class SpinBox(Widget):
 
     def create(self, parent):
         self.widget = self.creator(parent)
-        self.widget.bind('<Return>', lambda event: self.callback())
+        bind_enter(self.widget, lambda event: self.callback())
         self.value = self.initial
         return self.widget
 
@@ -263,7 +247,7 @@ class Entry(Widget):
         self.entry = self.creator(parent)
         self.value = self.initial
         if self.callback:
-            self.entry.bind('<Return>', self.callback)
+            bind_enter(self.entry, self.callback)
         return self.entry
 
     @property
@@ -418,18 +402,41 @@ class MenuItem:
         self.underline = label.find('_')
         self.label = label.replace('_', '')
 
+        is_macos = platform.system() == 'Darwin'
+
         if key:
-            if key[:4] == 'Ctrl':
-                self.keyname = f'<Control-{key[-1].lower()}>'
-            elif key[:3] == 'Alt':
-                self.keyname = f'<Alt-{key[-1].lower()}>'
+            parts = key.split('+')
+            modifiers = []
+            key_char = None
+
+            for part in parts:
+                if part in ('Alt', 'Shift'):
+                    modifiers.append(part)
+                elif part == 'Ctrl':
+                    modifiers.append('Control')
+                elif len(part) == 1 and 'Shift' in modifiers:
+                    # If shift and letter, uppercase
+                    key_char = part
+                else:
+                    # Lower case
+                    key_char = part.lower()
+
+            if is_macos:
+                modifiers = ['Command' if m == 'Alt' else m for m in modifiers]
+
+            if modifiers and key_char:
+                self.keyname = f"<{'-'.join(modifiers)}-{key_char}>"
             else:
+                # Handle special non-modifier keys
                 self.keyname = {
                     'Home': '<Home>',
                     'End': '<End>',
                     'Page-Up': '<Prior>',
                     'Page-Down': '<Next>',
-                    'Backspace': '<BackSpace>'}.get(key, key.lower())
+                    'Backspace': '<BackSpace>'
+                }.get(key, key.lower())
+        else:
+            self.keyname = None
 
         if key:
             def callback2(event=None):
@@ -440,7 +447,10 @@ class MenuItem:
         else:
             self.callback = callback
 
-        self.key = key
+        if is_macos and key is not None:
+            self.key = key.replace('Alt', 'Command')
+        else:
+            self.key = key
         self.value = value
         self.choices = choices
         self.submenu = submenu
@@ -498,7 +508,7 @@ class MenuItem:
 class MainWindow(BaseWindow):
     def __init__(self, title, close=None, menu=[]):
         self.win = tk.Tk()
-        BaseWindow.__init__(self, title, close)
+        super().__init__(title, close)
 
         # self.win.tk.call('tk', 'scaling', 3.0)
         # self.win.tk.call('tk', 'scaling', '-displayof', '.', 7)
@@ -547,7 +557,7 @@ class MainWindow(BaseWindow):
 
 def bind(callback, modifier=None):
     def handle(event):
-        event.button = mouse_buttons.get(event.num, event.num)
+        event.button = event.num
         event.key = event.keysym.lower()
         event.modifier = modifier
         callback(event)
@@ -557,13 +567,7 @@ def bind(callback, modifier=None):
 class ASEFileChooser(LoadFileDialog):
     def __init__(self, win, formatcallback=lambda event: None):
         from ase.io.formats import all_formats, get_ioformat
-        LoadFileDialog.__init__(self, win, _('Open ...'))
-        # fix tkinter not automatically setting dialog type
-        # remove from Python3.8+
-        # see https://github.com/python/cpython/pull/25187
-        # and https://bugs.python.org/issue43655
-        # and https://github.com/python/cpython/pull/25592
-        set_windowtype(self.top, 'dialog')
+        super().__init__(win, _('Open ...'))
         labels = [_('Automatic')]
         values = ['']
 
@@ -596,7 +600,7 @@ class ASEGUIWindow(MainWindow):
     def __init__(self, close, menu, config,
                  scroll, scroll_event,
                  press, move, release, resize):
-        MainWindow.__init__(self, 'ASE-GUI', close, menu)
+        super().__init__('ASE-GUI', close, menu)
 
         self.size = np.array([450, 450])
 
@@ -613,17 +617,17 @@ class ASEGUIWindow(MainWindow):
         self.status = tk.Label(self.win, text='', anchor=tk.W)
         self.status.pack(side=tk.BOTTOM, fill=tk.X)
 
-        right = mouse_buttons.get(3, 3)
         self.canvas.bind('<ButtonPress>', bind(press))
-        self.canvas.bind('<B1-Motion>', bind(move))
-        self.canvas.bind(f'<B{right}-Motion>', bind(move))
+        for button in range(1, 4):
+            self.canvas.bind(f'<B{button}-Motion>', bind(move))
         self.canvas.bind('<ButtonRelease>', bind(release))
         self.canvas.bind('<Control-ButtonRelease>', bind(release, 'ctrl'))
         self.canvas.bind('<Shift-ButtonRelease>', bind(release, 'shift'))
         self.canvas.bind('<Configure>', resize)
         if not config['swap_mouse']:
-            self.canvas.bind(f'<Shift-B{right}-Motion>',
-                             bind(scroll))
+            for button in (2, 3):
+                self.canvas.bind(f'<Shift-B{button}-Motion>',
+                                 bind(scroll))
         else:
             self.canvas.bind('<Shift-B1-Motion>',
                              bind(scroll))
@@ -684,3 +688,13 @@ class ASEGUIWindow(MainWindow):
         id = self.win.after(int(time * 1000), callback)
         # Quick'n'dirty object with a cancel() method:
         return namedtuple('Timer', 'cancel')(lambda: self.win.after_cancel(id))
+
+
+def bind_enter(widget, callback):
+    """Preferred incantation for binding Return/Enter.
+
+    Bindings work differently on different OSes.  This ensures that
+    keypad and normal Return work the same on Linux particularly."""
+
+    widget.bind('<Return>', callback)
+    widget.bind('<KP_Enter>', callback)
diff -pruN 3.24.0-1/ase/gui/view.py 3.26.0-1/ase/gui/view.py
--- 3.24.0-1/ase/gui/view.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/view.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from math import cos, sin, sqrt
 from os.path import basename
 
@@ -94,7 +96,6 @@ def get_bonds(atoms, covalent_radii):
 class View:
     def __init__(self, rotations):
         self.colormode = 'jmol'  # The default colors
-        self.labels = None
         self.axes = rotate(rotations)
         self.configured = False
         self.frame = None
@@ -108,6 +109,7 @@ class View:
         # scaling factors for vectors
         self.force_vector_scale = self.config['force_vector_scale']
         self.velocity_vector_scale = self.config['velocity_vector_scale']
+        self.magmom_vector_scale = self.config['magmom_vector_scale']
 
         # buttons
         self.b1 = 1  # left
@@ -204,6 +206,8 @@ class View:
             b[bonds[:, 2:].any(1)] *= 0.5
             self.B[ncellparts:] = self.X_bonds + b
 
+        self.obs.set_atoms.notify()
+
     def showing_bonds(self):
         return self.window['toggle-show-bonds']
 
@@ -213,22 +217,24 @@ class View:
     def toggle_show_unit_cell(self, key=None):
         self.set_frame()
 
-    def update_labels(self):
+    def get_labels(self):
         index = self.window['show-labels']
         if index == 0:
-            self.labels = None
-        elif index == 1:
-            self.labels = list(range(len(self.atoms)))
-        elif index == 2:
-            self.labels = list(get_magmoms(self.atoms))
-        elif index == 4:
+            return None
+
+        if index == 1:
+            return list(range(len(self.atoms)))
+
+        if index == 2:
+            return list(get_magmoms(self.atoms))
+
+        if index == 4:
             Q = self.atoms.get_initial_charges()
-            self.labels = [f'{q:.4g}' for q in Q]
-        else:
-            self.labels = self.atoms.get_chemical_symbols()
+            return [f'{q:.4g}' for q in Q]
+
+        return self.atoms.symbols
 
     def show_labels(self):
-        self.update_labels()
         self.draw()
 
     def toggle_show_axes(self, key=None):
@@ -251,6 +257,9 @@ class View:
     def toggle_show_forces(self, key=None):
         self.draw()
 
+    def toggle_show_magmoms(self, key=None):
+        self.draw()
+
     def hide_selected(self):
         self.images.visible[self.images.selected] = False
         self.draw()
@@ -267,7 +276,7 @@ class View:
 
     def colors_window(self, key=None):
         win = ColorWindow(self)
-        self.register_vulnerable(win)
+        self.obs.new_atoms.register(win.notify_atoms_changed)
         return win
 
     def focus(self, x=None):
@@ -313,25 +322,25 @@ class View:
             self.axes = rotate('-90.0x,-90.0y,0.0z')
         elif key == 'Y':
             self.axes = rotate('90.0x,0.0y,90.0z')
-        elif key == 'Alt+Z':
+        elif key == 'Shift+Z':
             self.axes = rotate('180.0x,0.0y,90.0z')
-        elif key == 'Alt+X':
+        elif key == 'Shift+X':
             self.axes = rotate('0.0x,90.0y,0.0z')
-        elif key == 'Alt+Y':
+        elif key == 'Shift+Y':
             self.axes = rotate('-90.0x,0.0y,0.0z')
         else:
-            if key == '3':
-                i, j = 0, 1
-            elif key == '1':
+            if key == 'I':
                 i, j = 1, 2
-            elif key == '2':
+            elif key == 'J':
                 i, j = 2, 0
-            elif key == 'Alt+3':
-                i, j = 1, 0
-            elif key == 'Alt+1':
+            elif key == 'K':
+                i, j = 0, 1
+            elif key == 'Shift+I':
                 i, j = 2, 1
-            elif key == 'Alt+2':
+            elif key == 'Shift+J':
                 i, j = 0, 2
+            elif key == 'Shift+K':
+                i, j = 1, 0
 
             A = complete_cell(self.atoms.cell)
             x1 = A[i]
@@ -432,6 +441,21 @@ class View:
             f = self.get_forces()
             vector_arrays.append(f * self.force_vector_scale)
 
+        if self.window['toggle-show-magmoms']:
+            magmom = get_magmoms(self.atoms)
+            # Turn this into a 3D vector if it is a scalar
+            magmom_vecs = []
+            for i in range(len(magmom)):
+                if isinstance(magmom[i], (int, float)):
+                    magmom_vecs.append(np.array([0, 0, magmom[i]]))
+                elif isinstance(magmom[i], np.ndarray) and len(magmom[i]) == 3:
+                    magmom_vecs.append(magmom[i])
+                else:
+                    raise TypeError('Magmom is not a 3-component vector '
+                                'or a scalar')
+            magmom_vecs = np.array(magmom_vecs)
+            vector_arrays.append(magmom_vecs * 0.5 * self.magmom_vector_scale)
+
         for array in vector_arrays:
             array[:] = np.dot(array, axes) + X[:n]
 
@@ -446,7 +470,7 @@ class View:
         ncell = len(self.X_cell)
         bond_linewidth = self.scale * 0.15
 
-        self.update_labels()
+        labels = self.get_labels()
 
         if self.arrowkey_mode == self.ARROWKEY_MOVE:
             movecolor = GREEN
@@ -497,10 +521,10 @@ class View:
                                A[a, 0], A[a, 1], A[a, 0] + ra, A[a, 1] + ra)
 
                     # Draw labels on the atoms
-                    if self.labels is not None:
+                    if labels is not None:
                         self.window.text(A[a, 0] + ra / 2,
                                          A[a, 1] + ra / 2,
-                                         str(self.labels[a]))
+                                         str(labels[a]))
 
                     # Draw cross on constrained atoms
                     if constrained[a]:
@@ -536,7 +560,16 @@ class View:
         self.window.update()
 
         if status:
-            self.status(self.atoms)
+            self.status.status(self.atoms)
+
+        # Currently we change the atoms all over the place willy-nilly
+        # and then call draw().  For which reason we abuse draw() to notify
+        # the observers about general changes.
+        #
+        # We should refactor so change_atoms is only emitted
+        # when when atoms actually change, and maybe have a separate signal
+        # to listen to e.g. changes of view.
+        self.obs.change_atoms.notify()
 
     def arrow(self, coords, width):
         line = self.window.line
diff -pruN 3.24.0-1/ase/gui/widgets.py 3.26.0-1/ase/gui/widgets.py
--- 3.24.0-1/ase/gui/widgets.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/gui/widgets.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import ase.data
 import ase.gui.ui as ui
 from ase import Atoms
@@ -92,5 +94,5 @@ def pywindow(title, callback):
             _('No Python code'),
             _('You have not (yet) specified a consistent set of parameters.'))
     else:
-        win = ui.Window(title, wmtype='utility')
+        win = ui.Window(title)
         win.add(ui.Text(code))
diff -pruN 3.24.0-1/ase/io/__init__.py 3.26.0-1/ase/io/__init__.py
--- 3.24.0-1/ase/io/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.io.bundletrajectory import BundleTrajectory
 from ase.io.formats import iread, read, string2index, write
 from ase.io.netcdftrajectory import NetCDFTrajectory
diff -pruN 3.24.0-1/ase/io/abinit.py 3.26.0-1/ase/io/abinit.py
--- 3.24.0-1/ase/io/abinit.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/abinit.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import re
 from glob import glob
diff -pruN 3.24.0-1/ase/io/acemolecule.py 3.26.0-1/ase/io/acemolecule.py
--- 3.24.0-1/ase/io/acemolecule.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/acemolecule.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 import ase.units
diff -pruN 3.24.0-1/ase/io/aff.py 3.26.0-1/ase/io/aff.py
--- 3.24.0-1/ase/io/aff.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/aff.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.io.ulm import DummyWriter, Reader, Writer
 from ase.io.ulm import InvalidULMFileError as InvalidAFFError
 from ase.io.ulm import open as affopen
diff -pruN 3.24.0-1/ase/io/aims.py 3.26.0-1/ase/io/aims.py
--- 3.24.0-1/ase/io/aims.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/aims.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Defines class/functions to write input and parse output for FHI-aims."""
 import os
 import re
diff -pruN 3.24.0-1/ase/io/amber.py 3.26.0-1/ase/io/amber.py
--- 3.24.0-1/ase/io/amber.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/amber.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 import ase.units as units
diff -pruN 3.24.0-1/ase/io/animation.py 3.26.0-1/ase/io/animation.py
--- 3.24.0-1/ase/io/animation.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/animation.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.visualize.plot import animate
 
 
diff -pruN 3.24.0-1/ase/io/bader.py 3.26.0-1/ase/io/bader.py
--- 3.24.0-1/ase/io/bader.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/bader.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.data import atomic_numbers
diff -pruN 3.24.0-1/ase/io/bundlemanipulate.py 3.26.0-1/ase/io/bundlemanipulate.py
--- 3.24.0-1/ase/io/bundlemanipulate.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/bundlemanipulate.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Functions for in-place manipulation of bundletrajectories.
 
 This module defines a number of functions that can be used to
diff -pruN 3.24.0-1/ase/io/bundletrajectory.py 3.26.0-1/ase/io/bundletrajectory.py
--- 3.24.0-1/ase/io/bundletrajectory.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/bundletrajectory.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """bundletrajectory - a module for I/O from large MD simulations.
 
 The BundleTrajectory class writes trajectory into a directory with the
diff -pruN 3.24.0-1/ase/io/castep/__init__.py 3.26.0-1/ase/io/castep/__init__.py
--- 3.24.0-1/ase/io/castep/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/castep/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module defines I/O routines with CASTEP files.
 The key idea is that all function accept or return  atoms objects.
 CASTEP specific parameters will be returned through the <atoms>.calc
@@ -27,6 +29,13 @@ from ase.parallel import paropen
 from ase.spacegroup import Spacegroup
 from ase.utils import atoms_to_spglib_cell, reader, writer
 
+from .geom_md_ts import (
+    read_castep_geom,
+    read_castep_md,
+    write_castep_geom,
+    write_castep_md,
+)
+
 units_ase = {
     'hbar': ase.units._hbar * ase.units.J,
     'Eh': ase.units.Hartree,
@@ -72,16 +81,17 @@ __all__ = [
     # routines for the generic io function
     'read_castep_castep',
     'read_castep_cell',
-    'read_geom',
     'read_castep_geom',
+    'read_castep_md',
     'read_phonon',
     'read_castep_phonon',
     # additional reads that still need to be wrapped
-    'read_md',
     'read_param',
     'read_seed',
     # write that is already wrapped
     'write_castep_cell',
+    'write_castep_geom',
+    'write_castep_md',
     # param write - in principle only necessary in junction with the calculator
     'write_param']
 
@@ -385,13 +395,13 @@ def read_freeform(fd):
     for i, l in enumerate(filelines):
 
         # Strip all comments, aka anything after a hash
-        L = re.split(r'[#!;]', l, 1)[0].strip()
+        L = re.split(r'[#!;]', l, maxsplit=1)[0].strip()
 
         if L == '':
             # Empty line... skip
             continue
 
-        lsplit = re.split(r'\s*[:=]*\s+', L, 1)
+        lsplit = re.split(r'\s*[:=]*\s+', L, maxsplit=1)
 
         if read_block:
             if lsplit[0].lower() == '%endblock':
@@ -760,80 +770,6 @@ def read_castep_cell(fd, index=None, cal
     return atoms
 
 
-def read_geom(filename, index=':', units=units_CODATA2002):
-    """
-    Wrapper function for the more generic read() functionality.
-
-    Note that this is function is intended to maintain backwards-compatibility
-    only. Keyword arguments will be passed to read_castep_geom().
-    """
-    from ase.io import read
-    return read(filename, index=index, format='castep-geom', units=units)
-
-
-def read_castep_geom(fd, index=None, units=units_CODATA2002):
-    """Reads a .geom file produced by the CASTEP GeometryOptimization task and
-    returns an atoms  object.
-    The information about total free energy and forces of each atom for every
-    relaxation step will be stored for further analysis especially in a
-    single-point calculator.
-    Note that everything in the .geom file is in atomic units, which has
-    been conversed to commonly used unit angstrom(length) and eV (energy).
-
-    Note that the index argument has no effect as of now.
-
-    Contribution by Wei-Bing Zhang. Thanks!
-
-    Routine now accepts a filedescriptor in order to out-source the gz and
-    bz2 handling to formats.py. Note that there is a fallback routine
-    read_geom() that behaves like previous versions did.
-    """
-    from ase.calculators.singlepoint import SinglePointCalculator
-
-    # fd is closed by embracing read() routine
-    txt = fd.readlines()
-
-    traj = []
-
-    Hartree = units['Eh']
-    Bohr = units['a0']
-
-    # Yeah, we know that...
-    # print('N.B.: Energy in .geom file is not 0K extrapolated.')
-    for i, line in enumerate(txt):
-        if line.find('<-- E') > 0:
-            start_found = True
-            energy = float(line.split()[0]) * Hartree
-            cell = [x.split()[0:3] for x in txt[i + 1:i + 4]]
-            cell = np.array([[float(col) * Bohr for col in row] for row in
-                             cell])
-        if line.find('<-- R') > 0 and start_found:
-            start_found = False
-            geom_start = i
-            for i, line in enumerate(txt[geom_start:]):
-                if line.find('<-- F') > 0:
-                    geom_stop = i + geom_start
-                    break
-            species = [line.split()[0] for line in
-                       txt[geom_start:geom_stop]]
-            geom = np.array([[float(col) * Bohr for col in
-                              line.split()[2:5]] for line in
-                             txt[geom_start:geom_stop]])
-            forces = np.array([[float(col) * Hartree / Bohr for col in
-                                line.split()[2:5]] for line in
-                               txt[geom_stop:geom_stop
-                                   + (geom_stop - geom_start)]])
-            image = ase.Atoms(species, geom, cell=cell, pbc=True)
-            image.calc = SinglePointCalculator(
-                atoms=image, energy=energy, forces=forces)
-            traj.append(image)
-
-    if index is None:
-        return traj
-    else:
-        return traj[index]
-
-
 def read_phonon(filename, index=None, read_vib_data=False,
                 gamma_only=True, frequency_factor=None,
                 units=units_CODATA2002):
@@ -963,166 +899,6 @@ def read_castep_phonon(fd, index=None, r
         return atoms
 
 
-def read_md(filename, index=None, return_scalars=False,
-            units=units_CODATA2002):
-    """Wrapper function for the more generic read() functionality.
-
-    Note that this function is intended to maintain backwards-compatibility
-    only. For documentation see read_castep_md()
-    """
-    if return_scalars:
-        full_output = True
-    else:
-        full_output = False
-
-    from ase.io import read
-    return read(filename, index=index, format='castep-md',
-                full_output=full_output, return_scalars=return_scalars,
-                units=units)
-
-
-def read_castep_md(fd, index=None, return_scalars=False,
-                   units=units_CODATA2002):
-    """Reads a .md file written by a CASTEP MolecularDynamics task
-    and returns the trajectory stored therein as a list of atoms object.
-
-    Note that the index argument has no effect as of now."""
-
-    from ase.calculators.singlepoint import SinglePointCalculator
-
-    factors = {
-        't': units['t0'] * 1E15,     # fs
-        'E': units['Eh'],            # eV
-        'T': units['Eh'] / units['kB'],
-        'P': units['Eh'] / units['a0']**3 * units['Pascal'],
-        'h': units['a0'],
-        'hv': units['a0'] / units['t0'],
-        'S': units['Eh'] / units['a0']**3,
-        'R': units['a0'],
-        'V': np.sqrt(units['Eh'] / units['me']),
-        'F': units['Eh'] / units['a0']}
-
-    # fd is closed by embracing read() routine
-    lines = fd.readlines()
-
-    L = 0
-    while 'END header' not in lines[L]:
-        L += 1
-    l_end_header = L
-    lines = lines[l_end_header + 1:]
-    times = []
-    energies = []
-    temperatures = []
-    pressures = []
-    traj = []
-
-    # Initialization
-    time = None
-    Epot = None
-    Ekin = None
-    EH = None
-    temperature = None
-    pressure = None
-    symbols = None
-    positions = None
-    cell = None
-    velocities = None
-    symbols = []
-    positions = []
-    velocities = []
-    forces = []
-    cell = np.eye(3)
-    cell_velocities = []
-    stress = []
-
-    for (L, line) in enumerate(lines):
-        fields = line.split()
-        if len(fields) == 0:
-            if L != 0:
-                times.append(time)
-                energies.append([Epot, EH, Ekin])
-                temperatures.append(temperature)
-                pressures.append(pressure)
-                atoms = ase.Atoms(symbols=symbols,
-                                  positions=positions,
-                                  cell=cell)
-                atoms.set_velocities(velocities)
-                if len(stress) == 0:
-                    atoms.calc = SinglePointCalculator(
-                        atoms=atoms, energy=Epot, forces=forces)
-                else:
-                    atoms.calc = SinglePointCalculator(
-                        atoms=atoms, energy=Epot,
-                        forces=forces, stress=stress)
-                traj.append(atoms)
-            symbols = []
-            positions = []
-            velocities = []
-            forces = []
-            cell = []
-            cell_velocities = []
-            stress = []
-            continue
-        if len(fields) == 1:
-            time = factors['t'] * float(fields[0])
-            continue
-
-        if fields[-1] == 'E':
-            E = [float(x) for x in fields[0:3]]
-            Epot, EH, Ekin = (factors['E'] * Ei for Ei in E)
-            continue
-
-        if fields[-1] == 'T':
-            temperature = factors['T'] * float(fields[0])
-            continue
-
-        # only printed in case of variable cell calculation or calculate_stress
-        # explicitly requested
-        if fields[-1] == 'P':
-            pressure = factors['P'] * float(fields[0])
-            continue
-        if fields[-1] == 'h':
-            h = [float(x) for x in fields[0:3]]
-            cell.append([factors['h'] * hi for hi in h])
-            continue
-
-        # only printed in case of variable cell calculation
-        if fields[-1] == 'hv':
-            hv = [float(x) for x in fields[0:3]]
-            cell_velocities.append([factors['hv'] * hvi for hvi in hv])
-            continue
-
-        # only printed in case of variable cell calculation
-        if fields[-1] == 'S':
-            S = [float(x) for x in fields[0:3]]
-            stress.append([factors['S'] * Si for Si in S])
-            continue
-        if fields[-1] == 'R':
-            symbols.append(fields[0])
-            R = [float(x) for x in fields[2:5]]
-            positions.append([factors['R'] * Ri for Ri in R])
-            continue
-        if fields[-1] == 'V':
-            V = [float(x) for x in fields[2:5]]
-            velocities.append([factors['V'] * Vi for Vi in V])
-            continue
-        if fields[-1] == 'F':
-            F = [float(x) for x in fields[2:5]]
-            forces.append([factors['F'] * Fi for Fi in F])
-            continue
-
-    if index is None:
-        pass
-    else:
-        traj = traj[index]
-
-    if return_scalars:
-        data = [times, energies, temperatures, pressures]
-        return data, traj
-    else:
-        return traj
-
-
 # Routines that only the calculator requires
 
 def read_param(filename='', calc=None, fd=None, get_interface_options=False):
diff -pruN 3.24.0-1/ase/io/castep/castep_input_file.py 3.26.0-1/ase/io/castep/castep_input_file.py
--- 3.24.0-1/ase/io/castep/castep_input_file.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/castep/castep_input_file.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import difflib
 import re
 import warnings
diff -pruN 3.24.0-1/ase/io/castep/castep_reader.py 3.26.0-1/ase/io/castep/castep_reader.py
--- 3.24.0-1/ase/io/castep/castep_reader.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/castep/castep_reader.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import io
 import re
 import warnings
@@ -324,9 +326,8 @@ def _read_header(out: io.TextIOBase):
             }[line.split(':')[-1].strip()]
 
         # Exchange-Correlation Parameters
-
         elif re.match(r'\susing functional\s*:', line):
-            parameters['xc_functional'] = {
+            functional_abbrevs = {
                 'Local Density Approximation': 'LDA',
                 'Perdew Wang (1991)': 'PW91',
                 'Perdew Burke Ernzerhof': 'PBE',
@@ -342,7 +343,16 @@ def _read_header(out: io.TextIOBase):
                 'hybrid HSE03': 'HSE03',
                 'hybrid HSE06': 'HSE06',
                 'RSCAN': 'RSCAN',
-            }[line.split(':')[-1].strip()]
+            }
+
+            # If the name is not recognised, use the whole string.
+            # This won't work in a new calculation, so will need to load from
+            # .param file in such cases... but at least it will fail rather
+            # than use the wrong XC!
+            _xc_full_name = line.split(':')[-1].strip()
+            parameters['xc_functional'] = functional_abbrevs.get(
+                _xc_full_name, _xc_full_name)
+
         elif 'DFT+D: Semi-empirical dispersion correction' in line:
             parameters['sedc_apply'] = _parse_on_off(line.split()[-1])
         elif 'SEDC with' in line:
diff -pruN 3.24.0-1/ase/io/castep/geom_md_ts.py 3.26.0-1/ase/io/castep/geom_md_ts.py
--- 3.24.0-1/ase/io/castep/geom_md_ts.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/io/castep/geom_md_ts.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,550 @@
+"""Parsers for CASTEP .geom, .md, .ts files"""
+
+from math import sqrt
+from typing import Callable, Dict, List, Optional, Sequence, TextIO, Union
+
+import numpy as np
+
+from ase import Atoms
+from ase.io.formats import string2index
+from ase.stress import full_3x3_to_voigt_6_stress, voigt_6_to_full_3x3_stress
+from ase.utils import reader, writer
+
+
+class Parser:
+    """Parser for <-- `key` in .geom, .md, .ts files"""
+
+    def __init__(self, units: Optional[Dict[str, float]] = None):
+        if units is None:
+            from ase.io.castep import units_CODATA2002
+
+            self.units = units_CODATA2002
+        else:
+            self.units = units
+
+    def parse(self, lines: List[str], key: str, method: Callable):
+        """Parse <-- `key` in `lines` using `method`"""
+        relevant_lines = [line for line in lines if line.strip().endswith(key)]
+        if relevant_lines:
+            return method(relevant_lines, self.units)
+        return None
+
+
+@reader
+def _read_images(
+    fd: TextIO,
+    index: Union[int, slice, str] = -1,
+    units: Optional[Dict[str, float]] = None,
+):
+    """Read a .geom or a .md file written by CASTEP.
+
+    - .geom: written by the CASTEP GeometryOptimization task
+    - .md: written by the CATSTEP MolecularDynamics task
+
+    Original contribution by Wei-Bing Zhang. Thanks!
+
+    Parameters
+    ----------
+    fd : str | TextIO
+        File name or object (possibly compressed with .gz and .bz2) to be read.
+    index : int | slice | str, default: -1
+        Index of image to be read.
+    units : dict[str, float], default: None
+        Dictionary with conversion factors from atomic units to ASE units.
+
+        - ``Eh``: Hartree energy in eV
+        - ``a0``: Bohr radius in Å
+        - ``me``: electron mass in Da
+        - ``kB``: Boltzmann constant in eV/K
+
+        If None, values based on CODATA2002 are used.
+
+    Returns
+    -------
+    Atoms | list[Atoms]
+        ASE Atoms object or list of them.
+
+    Notes
+    -----
+    The force-consistent energy, forces, stress are stored in ``atoms.calc``.
+
+    Everything in the .geom or the .md file is in atomic units.
+    They are converted in ASE units, i.e., Å (length), eV (energy), Da (mass).
+
+    Stress in the .geom or the .md file includes kinetic contribution, which is
+    subtracted in ``atoms.calc``.
+
+    """
+    if isinstance(index, str):
+        index = string2index(index)
+    if isinstance(index, str):
+        raise ValueError(index)
+    return list(_iread_images(fd, units))[index]
+
+
+read_castep_geom = _read_images
+read_castep_md = _read_images
+
+
+def _iread_images(fd: TextIO, units: Optional[Dict[str, float]] = None):
+    """Read a .geom or .md file of CASTEP MolecularDynamics as a generator."""
+    parser = Parser(units)
+    _read_header(fd)
+    lines = []
+    for line in fd:
+        if line.strip():
+            lines.append(line)
+        else:
+            yield _read_atoms(lines, parser)
+            lines = []
+
+
+iread_castep_geom = _iread_images
+iread_castep_md = _iread_images
+
+
+def _read_atoms(lines: List[str], parser: Parser) -> Atoms:
+    from ase.calculators.singlepoint import SinglePointCalculator
+
+    energy = parser.parse(lines, '<-- E', _read_energies)
+    cell = parser.parse(lines, '<-- h', _read_cell)
+    stress = parser.parse(lines, '<-- S', _read_stress)
+    symbols, positions = parser.parse(lines, '<-- R', _read_positions)
+    velocities = parser.parse(lines, '<-- V', _read_velocities)
+    forces = parser.parse(lines, '<-- F', _read_forces)
+
+    # Currently unused tags:
+    #
+    # temperature = parser.parse(lines, '<-- T', _read_temperature)
+    # pressure = parser.parse(lines, '<-- P', _read_pressure)
+    # cell_velocities = parser.extract(lines, '<-- hv', _read_cell_velocities)
+
+    atoms = Atoms(symbols, positions, cell=cell, pbc=True)
+
+    if velocities is not None:  # MolecularDynamics
+        units = parser.units
+        factor = units['a0'] * sqrt(units['me'] / units['Eh'])
+        atoms.info['time'] = float(lines[0].split()[0]) * factor  # au -> ASE
+        atoms.set_velocities(velocities)
+
+    if stress is not None:
+        stress -= atoms.get_kinetic_stress(voigt=True)
+
+    # The energy in .geom or .md file is the force-consistent one
+    # (possibly with the the finite-basis-set correction when, e.g.,
+    # finite_basis_corr!=0 in GeometryOptimisation).
+    # It should therefore be reasonable to assign it to `free_energy`.
+    # Be also aware that the energy in .geom file not 0K extrapolated.
+    atoms.calc = SinglePointCalculator(
+        atoms=atoms,
+        free_energy=energy,
+        forces=forces,
+        stress=stress,
+    )
+
+    return atoms
+
+
+def _read_header(fd: TextIO):
+    for line in fd:
+        if 'END header' in line:
+            next(fd)  # read blank line below 'END header'
+            break
+
+
+def _read_energies(lines: List[str], units: Dict[str, float]) -> float:
+    """Read force-consistent energy
+
+    Notes
+    -----
+    Enthalpy and kinetic energy (in .md) are also written in the same line.
+    They are however not parsed because they can be computed using stress and
+    atomic velocties, respsectively.
+    """
+    return float(lines[0].split()[0]) * units['Eh']
+
+
+def _read_temperature(lines: List[str], units: Dict[str, float]) -> float:
+    """Read temperature
+
+    Notes
+    -----
+    Temperature can be computed from kinetic energy and hence not necessary.
+    """
+    factor = units['Eh'] / units['kB']  # hartree -> K
+    return float(lines[0].split()[0]) * factor
+
+
+def _read_pressure(lines: List[str], units: Dict[str, float]) -> float:
+    """Read pressure
+
+    Notes
+    -----
+    Pressure can be computed from stress and hence not necessary.
+    """
+    factor = units['Eh'] / units['a0'] ** 3  # au -> eV/A3
+    return float(lines[0].split()[0]) * factor
+
+
+def _read_cell(lines: List[str], units: Dict[str, float]) -> np.ndarray:
+    bohr = units['a0']
+    cell = np.array([line.split()[0:3] for line in lines], dtype=float)
+    return cell * bohr
+
+
+# def _read_cell_velocities(lines: List[str], units: Dict[str, float]):
+#     hartree = units['Eh']
+#     me = units['me']
+#     cell_velocities = np.array([_.split()[0:3] for _ in lines], dtype=float)
+#     return cell_velocities * np.sqrt(hartree / me)
+
+
+def _read_stress(lines: List[str], units: Dict[str, float]) -> np.ndarray:
+    hartree = units['Eh']
+    bohr = units['a0']
+    stress = np.array([line.split()[0:3] for line in lines], dtype=float)
+    return full_3x3_to_voigt_6_stress(stress) * (hartree / bohr**3)
+
+
+def _read_positions(
+    lines: List[str], units: Dict[str, float]
+) -> tuple[list[str], np.ndarray]:
+    bohr = units['a0']
+    symbols = [line.split()[0] for line in lines]
+    positions = np.array([line.split()[2:5] for line in lines], dtype=float)
+    return symbols, positions * bohr
+
+
+def _read_velocities(lines: List[str], units: Dict[str, float]) -> np.ndarray:
+    hartree = units['Eh']
+    me = units['me']
+    velocities = np.array([line.split()[2:5] for line in lines], dtype=float)
+    return velocities * np.sqrt(hartree / me)
+
+
+def _read_forces(lines: List[str], units: Dict[str, float]) -> np.ndarray:
+    hartree = units['Eh']
+    bohr = units['a0']
+    forces = np.array([line.split()[2:5] for line in lines], dtype=float)
+    return forces * (hartree / bohr)
+
+
+@writer
+def write_castep_geom(
+    fd: TextIO,
+    images: Union[Atoms, Sequence[Atoms]],
+    units: Optional[Dict[str, float]] = None,
+    *,
+    pressure: float = 0.0,
+    sort: bool = False,
+):
+    """Write a CASTEP .geom file.
+
+    .. versionadded:: 3.25.0
+
+    Parameters
+    ----------
+    fd : str | TextIO
+        File name or object (possibly compressed with .gz and .bz2) to be read.
+    images : Atoms | Sequenece[Atoms]
+        ASE Atoms object(s) to be written.
+    units : dict[str, float], default: None
+        Dictionary with conversion factors from atomic units to ASE units.
+
+        - ``Eh``: Hartree energy in eV
+        - ``a0``: Bohr radius in Å
+        - ``me``: electron mass in Da
+        - ``kB``: Boltzmann constant in eV/K
+
+        If None, values based on CODATA2002 are used.
+    pressure : float, default: 0.0
+        External pressure in eV/Å\\ :sup:`3`.
+    sort : bool, default: False
+        If True, atoms are sorted in ascending order of atomic number.
+
+    Notes
+    -----
+    - Values in the .geom file are in atomic units.
+    - Stress is printed including kinetic contribution.
+
+    """
+    if isinstance(images, Atoms):
+        images = [images]
+
+    if units is None:
+        from ase.io.castep import units_CODATA2002
+
+        units = units_CODATA2002
+
+    _write_header(fd)
+
+    for index, atoms in enumerate(images):
+        if sort:
+            atoms = atoms[atoms.numbers.argsort()]
+        _write_convergence_status(fd, index)
+        _write_energies_geom(fd, atoms, units, pressure)
+        _write_cell(fd, atoms, units)
+        _write_stress(fd, atoms, units)
+        _write_positions(fd, atoms, units)
+        _write_forces(fd, atoms, units)
+        fd.write('  \n')
+
+
+@writer
+def write_castep_md(
+    fd: TextIO,
+    images: Union[Atoms, Sequence[Atoms]],
+    units: Optional[Dict[str, float]] = None,
+    *,
+    pressure: float = 0.0,
+    sort: bool = False,
+):
+    """Write a CASTEP .md file.
+
+    .. versionadded:: 3.25.0
+
+    Parameters
+    ----------
+    fd : str | TextIO
+        File name or object (possibly compressed with .gz and .bz2) to be read.
+    images : Atoms | Sequenece[Atoms]
+        ASE Atoms object(s) to be written.
+    units : dict[str, float], default: None
+        Dictionary with conversion factors from atomic units to ASE units.
+
+        - ``Eh``: Hartree energy in eV
+        - ``a0``: Bohr radius in Å
+        - ``me``: electron mass in Da
+        - ``kB``: Boltzmann constant in eV/K
+
+        If None, values based on CODATA2002 are used.
+    pressure : float, default: 0.0
+        External pressure in eV/Å\\ :sup:`3`.
+    sort : bool, default: False
+        If True, atoms are sorted in ascending order of atomic number.
+
+    Notes
+    -----
+    - Values in the .md file are in atomic units.
+    - Stress is printed including kinetic contribution.
+
+    """
+    if isinstance(images, Atoms):
+        images = [images]
+
+    if units is None:
+        from ase.io.castep import units_CODATA2002
+
+        units = units_CODATA2002
+
+    _write_header(fd)
+
+    for index, atoms in enumerate(images):
+        if sort:
+            atoms = atoms[atoms.numbers.argsort()]
+        _write_time(fd, index)
+        _write_energies_md(fd, atoms, units, pressure)
+        _write_temperature(fd, atoms, units)
+        _write_cell(fd, atoms, units)
+        _write_cell_velocities(fd, atoms, units)
+        _write_stress(fd, atoms, units)
+        _write_positions(fd, atoms, units)
+        _write_velocities(fd, atoms, units)
+        _write_forces(fd, atoms, units)
+        fd.write('  \n')
+
+
+def _format_float(x: float) -> str:
+    """Format a floating number for .geom and .md files"""
+    return np.format_float_scientific(
+        x,
+        precision=16,
+        unique=False,
+        pad_left=2,
+        exp_digits=3,
+    ).replace('e', 'E')
+
+
+def _write_header(fd: TextIO):
+    fd.write(' BEGIN header\n')
+    fd.write('  \n')
+    fd.write(' END header\n')
+    fd.write('  \n')
+
+
+def _write_convergence_status(fd: TextIO, index: int):
+    fd.write(21 * ' ')
+    fd.write(f'{index:18d}')
+    fd.write(34 * ' ')
+    for _ in range(4):
+        fd.write('   F')  # Convergence status. So far F for all.
+    fd.write(10 * ' ')
+    fd.write('  <-- c\n')
+
+
+def _write_time(fd: TextIO, index: int):
+    fd.write(18 * ' ' + f'   {_format_float(index)}\n')  # So far index.
+
+
+def _write_energies_geom(
+    fd: TextIO,
+    atoms: Atoms,
+    units: Dict[str, float],
+    pressure: float = 0.0,
+):
+    """Write energies (in hartree) in a CASTEP .geom file.
+
+    The energy and the enthalpy are printed.
+    """
+    hartree = units['Eh']
+    if atoms.calc is None:
+        return
+    if atoms.calc.results.get('free_energy') is None:
+        return
+    energy = atoms.calc.results.get('free_energy') / hartree
+    pv = pressure * atoms.get_volume() / hartree
+    fd.write(18 * ' ')
+    fd.write(f'   {_format_float(energy)}')
+    fd.write(f'   {_format_float(energy + pv)}')
+    fd.write(27 * ' ')
+    fd.write('  <-- E\n')
+
+
+def _write_energies_md(
+    fd: TextIO,
+    atoms: Atoms,
+    units: Dict[str, float],
+    pressure: float = 0.0,
+):
+    """Write energies (in hartree) in a CASTEP .md file.
+
+    The potential energy, the total energy or enthalpy, and the kinetic energy
+    are printed.
+
+    Notes
+    -----
+    For the second item, CASTEP prints the total energy for the NVE and the NVT
+    ensembles and the total enthalpy for the NPH and NPT ensembles.
+    For the Nosé–Hoover (chain) thermostat, furthermore, the energies of the
+    thermostats are also added.
+
+    """
+    hartree = units['Eh']
+    if atoms.calc is None:
+        return
+    if atoms.calc.results.get('free_energy') is None:
+        return
+    potential = atoms.calc.results.get('free_energy') / hartree
+    kinetic = atoms.get_kinetic_energy() / hartree
+    pv = pressure * atoms.get_volume() / hartree
+    fd.write(18 * ' ')
+    fd.write(f'   {_format_float(potential)}')
+    fd.write(f'   {_format_float(potential + kinetic + pv)}')
+    fd.write(f'   {_format_float(kinetic)}')
+    fd.write('  <-- E\n')
+
+
+def _write_temperature(fd: TextIO, atoms: Atoms, units: Dict[str, float]):
+    """Write temperature (in hartree) in a CASTEP .md file.
+
+    CASTEP writes the temperature in a .md file with 3`N` degrees of freedom
+    regardless of the given constraints including the fixed center of mass
+    (``FIX_COM`` which is by default ``TRUE`` in many cases).
+    To get the consistent result with the above behavior of CASTEP using
+    this method, we must set the corresponding constraints (e.g. ``FixCom``)
+    to the ``Atoms`` object beforehand.
+    """
+    hartree = units['Eh']  # eV
+    boltzmann = units['kB']  # eV/K
+    temperature = atoms.get_temperature() * boltzmann / hartree
+    fd.write(18 * ' ')
+    fd.write(f'   {_format_float(temperature)}')
+    fd.write(54 * ' ')
+    fd.write('  <-- T\n')
+
+
+def _write_cell(fd: TextIO, atoms: Atoms, units: Dict[str, float]):
+    bohr = units['a0']
+    cell = atoms.cell / bohr  # in bohr
+    for i in range(3):
+        fd.write(18 * ' ')
+        for j in range(3):
+            fd.write(f'   {_format_float(cell[i, j])}')
+        fd.write('  <-- h\n')
+
+
+def _write_cell_velocities(fd: TextIO, atoms: Atoms, units: Dict[str, float]):
+    pass  # TODO: to be implemented
+
+
+def _write_stress(fd: TextIO, atoms: Atoms, units: Dict[str, float]):
+    if atoms.calc is None:
+        return
+
+    stress = atoms.calc.results.get('stress')
+    if stress is None:
+        return
+
+    if stress.shape != (3, 3):
+        stress = voigt_6_to_full_3x3_stress(stress)
+
+    stress += atoms.get_kinetic_stress(voigt=False)
+
+    hartree = units['Eh']
+    bohr = units['a0']
+    stress = stress / (hartree / bohr**3)
+
+    for i in range(3):
+        fd.write(18 * ' ')
+        for j in range(3):
+            fd.write(f'   {_format_float(stress[i, j])}')
+        fd.write('  <-- S\n')
+
+
+def _write_positions(fd: TextIO, atoms: Atoms, units: Dict[str, float]):
+    bohr = units['a0']
+    positions = atoms.positions / bohr
+    symbols = atoms.symbols
+    indices = symbols.species_indices()
+    for i, symbol, position in zip(indices, symbols, positions):
+        fd.write(f' {symbol:8s}')
+        fd.write(f' {i + 1:8d}')
+        for j in range(3):
+            fd.write(f'   {_format_float(position[j])}')
+        fd.write('  <-- R\n')
+
+
+def _write_forces(fd: TextIO, atoms: Atoms, units: Dict[str, float]):
+    if atoms.calc is None:
+        return
+
+    forces = atoms.calc.results.get('forces')
+    if forces is None:
+        return
+
+    hartree = units['Eh']
+    bohr = units['a0']
+    forces = forces / (hartree / bohr)
+
+    symbols = atoms.symbols
+    indices = symbols.species_indices()
+    for i, symbol, force in zip(indices, symbols, forces):
+        fd.write(f' {symbol:8s}')
+        fd.write(f' {i + 1:8d}')
+        for j in range(3):
+            fd.write(f'   {_format_float(force[j])}')
+        fd.write('  <-- F\n')
+
+
+def _write_velocities(fd: TextIO, atoms: Atoms, units: Dict[str, float]):
+    hartree = units['Eh']
+    me = units['me']
+    velocities = atoms.get_velocities() / np.sqrt(hartree / me)
+    symbols = atoms.symbols
+    indices = symbols.species_indices()
+    for i, symbol, velocity in zip(indices, symbols, velocities):
+        fd.write(f' {symbol:8s}')
+        fd.write(f' {i + 1:8d}')
+        for j in range(3):
+            fd.write(f'   {_format_float(velocity[j])}')
+        fd.write('  <-- V\n')
diff -pruN 3.24.0-1/ase/io/cfg.py 3.26.0-1/ase/io/cfg.py
--- 3.24.0-1/ase/io/cfg.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/cfg.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 import ase
diff -pruN 3.24.0-1/ase/io/cif.py 3.26.0-1/ase/io/cif.py
--- 3.24.0-1/ase/io/cif.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/cif.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Module to read and write atoms in cif file format.
 
 See http://www.iucr.org/resources/cif/spec/version1.1/cifsyntax for a
diff -pruN 3.24.0-1/ase/io/cif_unicode.py 3.26.0-1/ase/io/cif_unicode.py
--- 3.24.0-1/ase/io/cif_unicode.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/cif_unicode.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 '''
 Conversion of text from a Crystallographic Information File (CIF) format to
 unicode. CIF text is neither unicode nor bibtex/latex code.
diff -pruN 3.24.0-1/ase/io/cjson.py 3.26.0-1/ase/io/cjson.py
--- 3.24.0-1/ase/io/cjson.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/cjson.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Module to read atoms in chemical json file format.
 
 https://wiki.openchemistry.org/Chemical_JSON
diff -pruN 3.24.0-1/ase/io/cp2k.py 3.26.0-1/ase/io/cp2k.py
--- 3.24.0-1/ase/io/cp2k.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/cp2k.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Reader for CP2Ks DCD_ALIGNED_CELL format and restart files.
 
diff -pruN 3.24.0-1/ase/io/crystal.py 3.26.0-1/ase/io/crystal.py
--- 3.24.0-1/ase/io/crystal.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/crystal.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.atoms import Atoms
 from ase.utils import reader, writer
 
diff -pruN 3.24.0-1/ase/io/cube.py 3.26.0-1/ase/io/cube.py
--- 3.24.0-1/ase/io/cube.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/cube.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 IO support for the Gaussian cube format.
 
diff -pruN 3.24.0-1/ase/io/dacapo.py 3.26.0-1/ase/io/dacapo.py
--- 3.24.0-1/ase/io/dacapo.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/dacapo.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.atom import Atom
diff -pruN 3.24.0-1/ase/io/db.py 3.26.0-1/ase/io/db.py
--- 3.24.0-1/ase/io/db.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/db.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,34 +3,33 @@ from ase.io.formats import string2index
 
 
 def read_db(filename, index, **kwargs):
-    db = ase.db.connect(filename, serial=True, **kwargs)
-
-    if isinstance(index, str):
-        try:
-            index = string2index(index)
-        except ValueError:
-            pass
-
-    if isinstance(index, int):
-        index = slice(index, index + 1 or None)
-
-    if isinstance(index, str):
-        # index is a database query string:
-        for row in db.select(index):
-            yield row.toatoms()
-    else:
-        start, stop, step = index.indices(db.count())
-        if start == stop:
-            return
-        assert step == 1
-        for row in db.select(offset=start, limit=stop - start):
-            yield row.toatoms()
+    with ase.db.connect(filename, serial=True, **kwargs) as db:
+        if isinstance(index, str):
+            try:
+                index = string2index(index)
+            except ValueError:
+                pass
+
+        if isinstance(index, int):
+            index = slice(index, index + 1 or None)
+
+        if isinstance(index, str):
+            # index is a database query string:
+            for row in db.select(index):
+                yield row.toatoms()
+        else:
+            start, stop, step = index.indices(db.count())
+            if start == stop:
+                return
+            assert step == 1
+            for row in db.select(offset=start, limit=stop - start):
+                yield row.toatoms()
 
 
 def write_db(filename, images, append=False, **kwargs):
-    con = ase.db.connect(filename, serial=True, append=append, **kwargs)
-    for atoms in images:
-        con.write(atoms)
+    with ase.db.connect(filename, serial=True, append=append, **kwargs) as con:
+        for atoms in images:
+            con.write(atoms)
 
 
 read_json = read_db
diff -pruN 3.24.0-1/ase/io/dftb.py 3.26.0-1/ase/io/dftb.py
--- 3.24.0-1/ase/io/dftb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/dftb.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import Sequence, Union
 
 import numpy as np
diff -pruN 3.24.0-1/ase/io/dlp4.py 3.26.0-1/ase/io/dlp4.py
--- 3.24.0-1/ase/io/dlp4.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/dlp4.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ Read/Write DL_POLY_4 CONFIG files """
 import itertools
 import re
@@ -244,8 +246,8 @@ def write_dlp4(fd: IO, atoms: Atoms,
         for row in atoms.get_cell():
             print("".join(map(float_format, row)), file=fd)
 
-    vels = atoms.get_velocities() / DLP_V_ASE if levcfg > 0 else []
-    forces = atoms.get_forces() / DLP_F_ASE if levcfg > 1 else []
+    vels = atoms.get_velocities() / DLP_V_ASE if levcfg > 0 else None
+    forces = atoms.get_forces() / DLP_F_ASE if levcfg > 1 else None
 
     labels = atoms.arrays.get(DLP4_LABELS_KEY)
 
diff -pruN 3.24.0-1/ase/io/dmol.py 3.26.0-1/ase/io/dmol.py
--- 3.24.0-1/ase/io/dmol.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/dmol.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 IO functions for DMol3 file formats.
 
diff -pruN 3.24.0-1/ase/io/elk.py 3.26.0-1/ase/io/elk.py
--- 3.24.0-1/ase/io/elk.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/elk.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import collections
 from pathlib import Path
 
@@ -91,7 +93,9 @@ def write_elk_in(fd, atoms, parameters=N
         parameters = {}
 
     parameters = dict(parameters)
+
     species_path = parameters.pop('species_dir', None)
+    species_path = parameters.pop('sppath', species_path)
 
     if parameters.get('spinpol') is None:
         if atoms.get_initial_magnetic_moments().any():
@@ -200,13 +204,13 @@ def write_elk_in(fd, atoms, parameters=N
         for a, m in species[symbol]:
             fd.write('%.14f %.14f %.14f 0.0 0.0 %.14f\n' %
                      (tuple(scaled[a]) + (m,)))
-
-    # if sppath is present in elk.in it overwrites species blocks!
+    fd.write('\n')
 
     # Elk seems to concatenate path and filename in such a way
     # that we must put a / at the end:
     if species_path is not None:
-        fd.write(f"sppath\n'{species_path}/'\n\n")
+        fd.write('sppath\n')
+        fd.write(f"'{species_path.rstrip('/')}/'\n\n")
 
 
 class ElkReader:
diff -pruN 3.24.0-1/ase/io/eon.py 3.26.0-1/ase/io/eon.py
--- 3.24.0-1/ase/io/eon.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/eon.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Copyright (C) 2012-2023, Jesper Friis, SINTEF
 # Copyright (C) 2024, Rohit Goswami, UI
 # (see accompanying license files for ASE).
diff -pruN 3.24.0-1/ase/io/eps.py 3.26.0-1/ase/io/eps.py
--- 3.24.0-1/ase/io/eps.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/eps.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import time
 
 from ase.io.utils import PlottingVariables, make_patch_list
diff -pruN 3.24.0-1/ase/io/espresso.py 3.26.0-1/ase/io/espresso.py
--- 3.24.0-1/ase/io/espresso.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/espresso.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Reads Quantum ESPRESSO files.
 
 Read multiple structures and results from pw.x output files. Read
@@ -198,7 +200,7 @@ def read_espresso_out(fileobj, index=sli
         else:
             if _PW_CELL in pwo_lines[image_index - 5]:
                 # CELL_PARAMETERS would be just before positions if present
-                cell, cell_alat = get_cell_parameters(
+                cell, _ = get_cell_parameters(
                     pwo_lines[image_index - 5:image_index])
             else:
                 cell = prev_structure.cell
diff -pruN 3.24.0-1/ase/io/espresso_namelist/keys.py 3.26.0-1/ase/io/espresso_namelist/keys.py
--- 3.24.0-1/ase/io/espresso_namelist/keys.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/espresso_namelist/keys.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 pw_keys = {
     "control": [
         "calculation",
diff -pruN 3.24.0-1/ase/io/espresso_namelist/namelist.py 3.26.0-1/ase/io/espresso_namelist/namelist.py
--- 3.24.0-1/ase/io/espresso_namelist/namelist.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/espresso_namelist/namelist.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 import warnings
 from collections import UserDict
diff -pruN 3.24.0-1/ase/io/exciting.py 3.26.0-1/ase/io/exciting.py
--- 3.24.0-1/ase/io/exciting.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/exciting.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This is the implementation of the exciting I/O functions.
 
 The main roles these functions do is write exciting ground state
diff -pruN 3.24.0-1/ase/io/extxyz.py 3.26.0-1/ase/io/extxyz.py
--- 3.24.0-1/ase/io/extxyz.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/extxyz.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Extended XYZ support
 
@@ -16,6 +18,7 @@ from io import StringIO, UnsupportedOper
 import numpy as np
 
 from ase.atoms import Atoms
+from ase.calculators.calculator import all_properties
 from ase.calculators.singlepoint import SinglePointCalculator
 from ase.constraints import FixAtoms, FixCartesian
 from ase.io.formats import index2range
@@ -45,10 +48,14 @@ UNPROCESSED_KEYS = {'uid'}
 
 SPECIAL_3_3_KEYS = {'Lattice', 'virial', 'stress'}
 
-# 'per-atom' and 'per-config'
+# Determine 'per-atom' and 'per-config' based on all_outputs shape,
+# but filter for things in all_properties because that's what
+# SinglePointCalculator accepts
 per_atom_properties = []
 per_config_properties = []
 for key, val in all_outputs.items():
+    if key not in all_properties:
+        continue
     if isinstance(val, ArrayProperty) and val.shapespec[0] == 'natoms':
         per_atom_properties.append(key)
     else:
@@ -502,9 +509,10 @@ def set_calc_and_arrays(atoms, arrays):
     results = {}
 
     for name, array in arrays.items():
-        if name in all_outputs:
+        if name in all_properties:
             results[name] = array
         else:
+            # store non-standard items in atoms.arrays
             atoms.new_array(name, array)
 
     for key in list(atoms.info):
@@ -830,7 +838,7 @@ def write_xyz(fileobj, images, comment='
                     voigt_6_to_full_3x3_stress(atoms.info['stress'])
 
         if columns is None:
-            fr_cols = (['symbols', 'positions']
+            fr_cols = (['symbols', 'positions', 'move_mask']
                        + [key for key in atoms.arrays if
                           key not in ['symbols', 'positions', 'numbers',
                                       'species', 'pos']])
@@ -885,7 +893,7 @@ def write_xyz(fileobj, images, comment='
 
         # Move mask
         if 'move_mask' in fr_cols:
-            cnstr = images[0]._get_constraints()
+            cnstr = images[0].constraints
             if len(cnstr) > 0:
                 c0 = cnstr[0]
                 if isinstance(c0, FixAtoms):
diff -pruN 3.24.0-1/ase/io/formats.py 3.26.0-1/ase/io/formats.py
--- 3.24.0-1/ase/io/formats.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/formats.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """File formats.
 
 This module implements the read(), iread() and write() functions in ase.io.
@@ -23,12 +25,12 @@ import sys
 import warnings
 from importlib import import_module
 from importlib.metadata import entry_points
-from pathlib import Path, PurePath
+from pathlib import PurePath
 from typing import (
     IO,
     Any,
     Dict,
-    Iterable,
+    Iterator,
     List,
     Optional,
     Sequence,
@@ -66,27 +68,6 @@ class IOFormat:
         self.magic: List[str] = []
         self.magic_regex: Optional[bytes] = None
 
-    def open(self, fname, mode: str = 'r') -> IO:
-        # We might want append mode, too
-        # We can allow more flags as needed (buffering etc.)
-        if mode not in list('rwa'):
-            raise ValueError("Only modes allowed are 'r', 'w', and 'a'")
-        if mode == 'r' and not self.can_read:
-            raise NotImplementedError('No reader implemented for {} format'
-                                      .format(self.name))
-        if mode == 'w' and not self.can_write:
-            raise NotImplementedError('No writer implemented for {} format'
-                                      .format(self.name))
-        if mode == 'a' and not self.can_append:
-            raise NotImplementedError('Appending not supported by {} format'
-                                      .format(self.name))
-
-        if self.isbinary:
-            mode += 'b'
-
-        path = Path(fname)
-        return path.open(mode, encoding=self.encoding)
-
     def _buf_as_filelike(self, data: Union[str, bytes]) -> IO:
         encoding = self.encoding
         if encoding is None:
@@ -359,6 +340,8 @@ F('aims', 'FHI-aims geometry file', '1S'
 F('aims-output', 'FHI-aims output', '+S',
   module='aims', magic=b'*Invoking FHI-aims ...')
 F('bundletrajectory', 'ASE bundle trajectory', '+S')
+# XXX: Define plugin in ase db backends package:
+# F('aselmdb', 'ASE LMDB format', '+F')
 F('castep-castep', 'CASTEP output file', '+F',
   module='castep', ext='castep')
 F('castep-cell', 'CASTEP geom file', '1F',
@@ -469,6 +452,8 @@ F('onetep-in', 'ONETEP input file', '1F'
   magic=[b'*lock species ',
          b'*LOCK SPECIES ',
          b'*--- INPUT FILE ---*'])
+F('orca-output', 'ORCA output', '+F',
+  module='orca', magic=b'* O   R   C   A *')
 F('proteindatabank', 'Protein Data Bank', '+F',
   ext='pdb')
 F('png', 'Portable Network Graphics', '1B')
@@ -817,7 +802,7 @@ def iread(
         parallel: bool = True,
         do_not_split_by_at_sign: bool = False,
         **kwargs
-) -> Iterable[Atoms]:
+) -> Iterator[Atoms]:
     """Iterator for reading Atoms objects from file.
 
     Works as the `read` function, but yields one Atoms object at a time
@@ -949,6 +934,9 @@ def filetype(
         if filename.startswith('mysql') or filename.startswith('mariadb'):
             return 'mysql'
 
+        if filename.endswith('aselmdb'):
+            return 'db'
+
         # strip any compression extensions that can be read
         root, _compression = get_compression(filename)
         basename = os.path.basename(root)
diff -pruN 3.24.0-1/ase/io/gamess_us.py 3.26.0-1/ase/io/gamess_us.py
--- 3.24.0-1/ase/io/gamess_us.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/gamess_us.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import re
 from copy import deepcopy
diff -pruN 3.24.0-1/ase/io/gaussian.py 3.26.0-1/ase/io/gaussian.py
--- 3.24.0-1/ase/io/gaussian.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/gaussian.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import logging
 import re
 import warnings
@@ -713,7 +715,7 @@ def _read_zmatrix(zmatrix_contents, zmat
     (zmatrix_vars), and returns atom positions and symbols '''
     try:
         atoms = parse_zmatrix(zmatrix_contents, defs=zmatrix_vars)
-    except (ValueError, AssertionError) as e:
+    except (ValueError, RuntimeError) as e:
         raise ParseError("Failed to read Z-matrix from "
                          "Gaussian input file: ", e)
     except KeyError as e:
diff -pruN 3.24.0-1/ase/io/gen.py 3.26.0-1/ase/io/gen.py
--- 3.24.0-1/ase/io/gen.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/gen.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Extension to ASE: read and write structures in GEN format
 
 Refer to DFTB+ manual for GEN format description.
diff -pruN 3.24.0-1/ase/io/gpaw_out.py 3.26.0-1/ase/io/gpaw_out.py
--- 3.24.0-1/ase/io/gpaw_out.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/gpaw_out.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 from typing import List, Tuple, Union
 
@@ -12,7 +14,7 @@ from ase.calculators.singlepoint import
 
 def index_startswith(lines: List[str], string: str) -> int:
     for i, line in enumerate(lines):
-        if line.startswith(string):
+        if line.strip().startswith(string):
             return i
     raise ValueError
 
@@ -54,6 +56,14 @@ def read_gpaw_out(fileobj, index):  # ->
     """Read text output from GPAW calculation."""
     lines = [line.lower() for line in fileobj.readlines()]
 
+    # read charge
+    try:
+        ii = index_startswith(lines, 'total charge:')
+    except ValueError:
+        q = None
+    else:
+        q = float(lines[ii].split()[2])
+
     blocks = []
     i1 = 0
     for i2, line in enumerate(lines):
@@ -115,13 +125,13 @@ def read_gpaw_out(fileobj, index):  # ->
             ibz_kpts = None
 
         try:
-            i = index_startswith(lines, 'energy contributions relative to')
+            i = index_startswith(lines, 'energy contributions relative to') + 2
         except ValueError:
             e = energy_contributions = None
         else:
             energy_contributions = {}
-            for line in lines[i + 2:i + 13]:
-                fields = line.split(':')
+            while i < len(lines):
+                fields = lines[i].split(':')
                 if len(fields) == 2:
                     name = fields[0]
                     energy = float(fields[1])
@@ -129,6 +139,7 @@ def read_gpaw_out(fileobj, index):  # ->
                     if name in ['zero kelvin', 'extrapolated']:
                         e = energy
                         break
+                i += 1
             else:  # no break
                 raise ValueError
 
@@ -151,11 +162,11 @@ def read_gpaw_out(fileobj, index):  # ->
         # read Eigenvalues and occupations
         ii1 = ii2 = 1e32
         try:
-            ii1 = index_startswith(lines, ' band   eigenvalues  occupancy')
+            ii1 = index_startswith(lines, 'band   eigenvalues  occupancy')
         except ValueError:
             pass
         try:
-            ii2 = index_startswith(lines, ' band  eigenvalues  occupancy')
+            ii2 = index_startswith(lines, 'band  eigenvalues  occupancy')
         except ValueError:
             pass
         ii = min(ii1, ii2)
@@ -177,13 +188,6 @@ def read_gpaw_out(fileobj, index):  # ->
                 kpts.append(SinglePointKPoint(1, 1, 0))
                 kpts[1].eps_n = vals[3]
                 kpts[1].f_n = vals[4]
-        # read charge
-        try:
-            ii = index_startswith(lines, 'total charge:')
-        except ValueError:
-            q = None
-        else:
-            q = float(lines[ii].split()[2])
         # read dipole moment
         try:
             ii = index_startswith(lines, 'dipole moment:')
diff -pruN 3.24.0-1/ase/io/gpumd.py 3.26.0-1/ase/io/gpumd.py
--- 3.24.0-1/ase/io/gpumd.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/gpumd.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/io/gpw.py 3.26.0-1/ase/io/gpw.py
--- 3.24.0-1/ase/io/gpw.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/gpw.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Read gpw-file from GPAW."""
 import ase.io.ulm as ulm
 from ase import Atoms
diff -pruN 3.24.0-1/ase/io/gromacs.py 3.26.0-1/ase/io/gromacs.py
--- 3.24.0-1/ase/io/gromacs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/gromacs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 read and write gromacs geometry files
 """
diff -pruN 3.24.0-1/ase/io/gromos.py 3.26.0-1/ase/io/gromos.py
--- 3.24.0-1/ase/io/gromos.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/gromos.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ write gromos96 geometry files
 (the exact file format is copied from the freely available
 gromacs package, http://www.gromacs.org
diff -pruN 3.24.0-1/ase/io/jsonio.py 3.26.0-1/ase/io/jsonio.py
--- 3.24.0-1/ase/io/jsonio.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/jsonio.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import datetime
 import json
 
@@ -40,6 +42,8 @@ def default(obj):
                                 flatobj.tolist())}
     if isinstance(obj, np.integer):
         return int(obj)
+    if isinstance(obj, np.floating):
+        return float(obj)
     if isinstance(obj, np.bool_):
         return bool(obj)
     if isinstance(obj, datetime.datetime):
diff -pruN 3.24.0-1/ase/io/jsv.py 3.26.0-1/ase/io/jsv.py
--- 3.24.0-1/ase/io/jsv.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/jsv.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 A module for reading and writing crystal structures from JSV
 See http://www.jcrystal.com/steffenweber/JAVA/JSV/jsv.html
diff -pruN 3.24.0-1/ase/io/lammpsdata.py 3.26.0-1/ase/io/lammpsdata.py
--- 3.24.0-1/ase/io/lammpsdata.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/lammpsdata.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 import warnings
 
diff -pruN 3.24.0-1/ase/io/lammpsrun.py 3.26.0-1/ase/io/lammpsrun.py
--- 3.24.0-1/ase/io/lammpsrun.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/lammpsrun.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import gzip
 import struct
 from collections import deque
@@ -8,8 +10,8 @@ import numpy as np
 from ase.atoms import Atoms
 from ase.calculators.lammps import convert
 from ase.calculators.singlepoint import SinglePointCalculator
+from ase.data import atomic_masses, chemical_symbols
 from ase.parallel import paropen
-from ase.quaternions import Quaternions
 
 
 def read_lammps_dump(infileobj, **kwargs):
@@ -102,6 +104,12 @@ def lammps_data_to_ase_atoms(
     if "element" in colnames:
         # priority to elements written in file
         elements = data[:, colnames.index("element")]
+    elif "mass" in colnames:
+        # try to determine elements from masses
+        elements = [
+            _mass2element(m)
+            for m in data[:, colnames.index("mass")].astype(float)
+        ]
     elif "type" in colnames:
         # fall back to `types` otherwise
         elements = data[:, colnames.index("type")].astype(int)
@@ -158,14 +166,14 @@ def lammps_data_to_ase_atoms(
         cell = prismobj.update_cell(cell)
 
     if quaternions is not None:
-        out_atoms = Quaternions(
+        out_atoms = atomsobj(
             symbols=elements,
             positions=positions,
             cell=cell,
             celldisp=celldisp,
             pbc=pbc,
-            quaternions=quaternions,
         )
+        out_atoms.new_array('quaternions', quaternions, dtype=float)
     elif positions is not None:
         # reverse coordinations transform to lammps system
         # (for all vectors = pos, vel, force)
@@ -207,12 +215,18 @@ def lammps_data_to_ase_atoms(
     # process the extra columns of fixes, variables and computes
     #    that can be dumped, add as additional arrays to atoms object
     for colname in colnames:
-        # determine if it is a compute or fix (but not the quaternian)
+        # determine if it is a compute, fix or
+        # custom property/atom (but not the quaternian)
         if (colname.startswith('f_') or colname.startswith('v_') or
-                (colname.startswith('c_') and not colname.startswith('c_q['))):
+            colname.startswith('d_') or colname.startswith('d2_') or
+            (colname.startswith('c_') and not colname.startswith('c_q['))):
             out_atoms.new_array(colname, get_quantity([colname]),
                                 dtype='float')
 
+        elif colname.startswith('i_') or colname.startswith('i2_'):
+            out_atoms.new_array(colname, get_quantity([colname]),
+                                dtype='int')
+
     return out_atoms
 
 
@@ -264,16 +278,16 @@ def read_lammps_dump_text(fileobj, index
     images = []
 
     # avoid references before assignment in case of incorrect file structure
-    cell, celldisp, pbc = None, None, False
+    cell, celldisp, pbc, info = None, None, False, {}
 
     while len(lines) > n_atoms:
         line = lines.popleft()
 
         if "ITEM: TIMESTEP" in line:
-            n_atoms = 0
             line = lines.popleft()
             # !TODO: pyflakes complains about this line -> do something
-            # ntimestep = int(line.split()[0])  # NOQA
+            ntimestep = int(line.split()[0])  # NOQA
+            info["timestep"] = ntimestep
 
         if "ITEM: NUMBER OF ATOMS" in line:
             line = lines.popleft()
@@ -323,8 +337,9 @@ def read_lammps_dump_text(fileobj, index
                 celldisp=celldisp,
                 atomsobj=Atoms,
                 pbc=pbc,
-                **kwargs
+                **kwargs,
             )
+            out_atoms.info.update(info)
             images.append(out_atoms)
 
         if len(images) > index_end >= 0:
@@ -479,3 +494,15 @@ def read_lammps_dump_binary(
             break
 
     return images[index]
+
+
+def _mass2element(mass):
+    """
+    Guess the element corresponding to a given atomic mass.
+
+    :param mass: Atomic mass for searching.
+    :return: Element symbol as a string.
+    """
+    min_idx = np.argmin(np.abs(atomic_masses - mass))
+    element = chemical_symbols[min_idx]
+    return element
diff -pruN 3.24.0-1/ase/io/magres.py 3.26.0-1/ase/io/magres.py
--- 3.24.0-1/ase/io/magres.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/magres.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module provides I/O functions for the MAGRES file format, introduced
 by CASTEP as an output format to store structural data and ab-initio
 calculated NMR parameters.
@@ -138,9 +140,7 @@ def read_magres(fd, include_unrecognised
 
         # Atom label, atom index and 3x3 tensor
         def sitensor33(name):
-            return lambda d: {'atom': {'label': data[0],
-                                       'index': int(data[1])},
-                              name: tensor33([float(x) for x in data[2:]])}
+            return lambda d: _parse_sitensor33(name, data)
 
         # 2x(Atom label, atom index) and 3x3 tensor
         def sisitensor33(name):
@@ -174,6 +174,79 @@ def read_magres(fd, include_unrecognised
 
         return data_dict
 
+    def _unmunge_label_index(label_index: str) -> tuple[str, str]:
+        """Splits a label_index string into a label and an index,
+        where the index is always the final 3 digits.
+
+        This function handles cases where the site label and index are combined
+        in CASTEP magres files (versions < 23),
+        e.g., 'H1222' instead of 'H1' and '222'.
+
+        Since site labels can contain numbers (e.g., H1, H2, H1a),
+        we extract the index as the final 3 digits.
+        The remaining characters form the label.
+
+        Note: Only call this function when label and index are confirmed
+        to be combined (detected by the line having 10 fields instead of 11).
+
+        Parameters
+        ----------
+        label_index : str
+            The input string containing the combined label and index
+            (e.g., 'H1222')
+
+        Returns
+        -------
+        tuple[str, str]
+            A tuple of (label, index) strings (e.g., ('H1', '222'))
+
+        Raises
+        ------
+        RuntimeError
+            If the index is >999 (not supported by this solution))
+            If invalid data format or regex match failure
+
+        Examples
+        --------
+        >>> _unmunge_label_index('H1222')
+        ('H1', '222')
+        >>> _unmunge_label_index('C201')
+        ('C', '201')
+        >>> _unmunge_label_index('H23104')
+        ('H23', '104')
+        >>> _unmunge_label_index('H1a100')
+        ('H1a', '100')
+        """
+        match = re.match(r'(.+?)(\d{3})$', label_index)
+        if match:
+            label, index = match.groups()
+            if not isinstance(label, str) or not isinstance(index, str):
+                raise RuntimeError("Regex match produced non-string values")
+            if index == '000':
+                raise RuntimeError(
+                    "Index greater than 999 detected. This is not supported in "
+                    "magres files with munged label and indices. "
+                    "Try manually unmunging the label and index."
+                )
+            return (label, index)
+        raise RuntimeError('Invalid data in magres block. '
+                          'Check the site labels and indices.')
+
+    def _parse_sitensor33(name, data):
+        # We expect label, index, and then the 3x3 tensor
+        if len(data) == 10:
+            label, index = _unmunge_label_index(data[0])
+            data = [label, index] + data[1:]
+        if len(data) != 11:
+            raise ValueError(
+                f"Expected 11 values for {name} tensor data, "
+                f"got {len(data)}"
+            )
+
+        return {'atom': {'label': data[0],
+                         'index': int(data[1])},
+                name: tensor33([float(x) for x in data[2:]])}
+
     def parse_atoms_block(block):
         """
             Parse atoms block into data dictionary given list of record tuples.
@@ -240,12 +313,6 @@ def read_magres(fd, include_unrecognised
 
     file_contents = fd.read()
 
-    # Solve incompatibility for atomic indices above 100
-    pattern_ms = r'(ms [a-zA-Z]{1,2})(\d+)'
-    file_contents = re.sub(pattern_ms, r'\g<1> \g<2>', file_contents)
-    pattern_efg = r'(efg [a-zA-Z]{1,2})(\d+)'
-    file_contents = re.sub(pattern_efg, r'\g<1> \g<2>', file_contents)
-
     # This works as a validity check
     version = get_version(file_contents)
     if version is None:
diff -pruN 3.24.0-1/ase/io/mol.py 3.26.0-1/ase/io/mol.py
--- 3.24.0-1/ase/io/mol.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/mol.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Reads chemical data in MDL Molfile format.
 
 See https://en.wikipedia.org/wiki/Chemical_table_file
diff -pruN 3.24.0-1/ase/io/mustem.py 3.26.0-1/ase/io/mustem.py
--- 3.24.0-1/ase/io/mustem.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/mustem.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Module to read and write atoms in xtl file format for the muSTEM software.
 
 See http://tcmp.ph.unimelb.edu.au/mustem/muSTEM.html for a few examples of
diff -pruN 3.24.0-1/ase/io/netcdftrajectory.py 3.26.0-1/ase/io/netcdftrajectory.py
--- 3.24.0-1/ase/io/netcdftrajectory.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/netcdftrajectory.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 netcdftrajectory - I/O trajectory files in the AMBER NetCDF convention
 
diff -pruN 3.24.0-1/ase/io/nomad_json.py 3.26.0-1/ase/io/nomad_json.py
--- 3.24.0-1/ase/io/nomad_json.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/nomad_json.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.nomad import read as _read_nomad_json
 
 
diff -pruN 3.24.0-1/ase/io/nwchem/nwreader.py 3.26.0-1/ase/io/nwchem/nwreader.py
--- 3.24.0-1/ase/io/nwchem/nwreader.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/nwchem/nwreader.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 from collections import OrderedDict
 
diff -pruN 3.24.0-1/ase/io/nwchem/nwreader_in.py 3.26.0-1/ase/io/nwchem/nwreader_in.py
--- 3.24.0-1/ase/io/nwchem/nwreader_in.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/nwchem/nwreader_in.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 
 import numpy as np
diff -pruN 3.24.0-1/ase/io/nwchem/nwwriter.py 3.26.0-1/ase/io/nwchem/nwwriter.py
--- 3.24.0-1/ase/io/nwchem/nwwriter.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/nwchem/nwwriter.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import random
 import string
diff -pruN 3.24.0-1/ase/io/octopus/input.py 3.26.0-1/ase/io/octopus/input.py
--- 3.24.0-1/ase/io/octopus/input.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/octopus/input.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import re
 
diff -pruN 3.24.0-1/ase/io/octopus/output.py 3.26.0-1/ase/io/octopus/output.py
--- 3.24.0-1/ase/io/octopus/output.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/octopus/output.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 
 import numpy as np
diff -pruN 3.24.0-1/ase/io/onetep.py 3.26.0-1/ase/io/onetep.py
--- 3.24.0-1/ase/io/onetep.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/onetep.py	2025-08-12 11:26:23.000000000 +0000
@@ -12,56 +12,56 @@ from ase.cell import Cell
 from ase.units import Bohr
 
 no_positions_error = (
-    "no positions can be read from this onetep output "
-    "if you wish to use ASE to read onetep outputs "
-    "please use uppercase block positions in your calculations"
+    'no positions can be read from this onetep output '
+    'if you wish to use ASE to read onetep outputs '
+    'please use uppercase block positions in your calculations'
 )
 
-unable_to_read = "unable to read this onetep output file, ending"
+unable_to_read = 'unable to read this onetep output file, ending'
 
 # taken from onetep source code,
 # does not seem to be from any known NIST data
-units = {"Hartree": 27.2116529, "Bohr": 1 / 1.889726134583548707935}
+units = {'Hartree': 27.2116529, 'Bohr': 1 / 1.889726134583548707935}
 
 # Want to add a functionality? add a global constant below
 ONETEP_START = re.compile(
-    r"(?i)^\s*\|\s*Linear-Scaling\s*Ab\s*"
-    r"Initio\s*Total\s*Energy\s*Program\s*\|\s*$"
+    r'(?i)^\s*\|\s*Linear-Scaling\s*Ab\s*'
+    r'Initio\s*Total\s*Energy\s*Program\s*\|\s*$'
 )
-ONETEP_STOP = re.compile(r"(?i)^\s*-+\s*TIMING\s*INFORMATION\s*-+\s*$")
+ONETEP_STOP = re.compile(r'(?i)^\s*-+\s*TIMING\s*INFORMATION\s*-+\s*$')
 ONETEP_TOTAL_ENERGY = re.compile(
-    r"(?i)^\s*\|\s*\*{3}\s*NGWF\s*" r"optimisation\s*converged\s*\*{3}\s*\|\s*$"
+    r'(?i)^\s*\|\s*\*{3}\s*NGWF\s*' r'optimisation\s*converged\s*\*{3}\s*\|\s*$'
 )
-ONETEP_FORCE = re.compile(r"(?i)^\s*\*+\s*Forces\s*\*+\s*$")
-ONETEP_MULLIKEN = re.compile(r"(?i)^\s*Mulliken\s*Atomic\s*Populations\s*$")
-ONETEP_SPIN = re.compile(r"(?i)^\s*Down\s*spin\s*density")
-ONETEP_POSITION = re.compile(r"(?i)^\s*Cell\s*Contents\s*$")
+ONETEP_FORCE = re.compile(r'(?i)^\s*\*+\s*Forces\s*\*+\s*$')
+ONETEP_MULLIKEN = re.compile(r'(?i)^\s*Mulliken\s*Atomic\s*Populations\s*$')
+ONETEP_SPIN = re.compile(r'(?i)^\s*Down\s*spin\s*density')
+ONETEP_POSITION = re.compile(r'(?i)^\s*Cell\s*Contents\s*$')
 ONETEP_FIRST_POSITION = re.compile(
-    r"^\s*%BLOCK\s*POSITIONS\s*_?\s*(ABS|FRAC)\s*:?\s*([*#!].*)?$"
+    r'^\s*%BLOCK\s*POSITIONS\s*_?\s*(ABS|FRAC)\s*:?\s*([*#!].*)?$'
 )
 ONETEP_WRONG_FIRST_POSITION = re.compile(
-    r"^\s*%block\s*positions\s*_?\s*(abs|frac)\s*:?\s*([*#!].*)?$"
+    r'^\s*%block\s*positions\s*_?\s*(abs|frac)\s*:?\s*([*#!].*)?$'
 )
 ONETEP_RESUMING_GEOM = re.compile(
-    r"(?i)^\s*<{16}\s*Resuming\s*previous"
-    r"\s*ONETEP\s*Geometry\s*Optimisation\s*>{16}\s*$"
+    r'(?i)^\s*<{16}\s*Resuming\s*previous'
+    r'\s*ONETEP\s*Geometry\s*Optimisation\s*>{16}\s*$'
 )
 
-ONETEP_ATOM_COUNT = re.compile(r"(?i)^\s*Totals\s*:\s*(\d+\s*)*$")
-ONETEP_IBFGS_ITER = re.compile(r"(?i)^\s*BFGS\s*:\s*starting\s*iteration")
-ONETEP_IBFGS_IMPROVE = re.compile(r"(?i)^\s*BFGS\s*:\s*improving\s*iteration")
+ONETEP_ATOM_COUNT = re.compile(r'(?i)^\s*Totals\s*:\s*(\d+\s*)*$')
+ONETEP_IBFGS_ITER = re.compile(r'(?i)^\s*BFGS\s*:\s*starting\s*iteration')
+ONETEP_IBFGS_IMPROVE = re.compile(r'(?i)^\s*BFGS\s*:\s*improving\s*iteration')
 ONETEP_START_GEOM = re.compile(
-    r"(?i)^<+\s*Starting\s*ONETEP\s*Geometry\s*Optimisation\s*>+$"
+    r'(?i)^<+\s*Starting\s*ONETEP\s*Geometry\s*Optimisation\s*>+$'
 )
-ONETEP_END_GEOM = re.compile(r"(?i)^\s*BFGS\s*:\s*Final\s*Configuration:\s*$")
+ONETEP_END_GEOM = re.compile(r'(?i)^\s*BFGS\s*:\s*Final\s*Configuration:\s*$')
 
-ONETEP_SPECIES = re.compile(r"(?i)^\s*%BLOCK\s*SPECIES\s*:?\s*([*#!].*)?$")
+ONETEP_SPECIES = re.compile(r'(?i)^\s*%BLOCK\s*SPECIES\s*:?\s*([*#!].*)?$')
 
 ONETEP_FIRST_CELL = re.compile(
-    r"(?i)^\s*%BLOCK\s*LATTICE\s*_?\s*CART\s*:?\s*([*#!].*)?$"
+    r'(?i)^\s*%BLOCK\s*LATTICE\s*_?\s*CART\s*:?\s*([*#!].*)?$'
 )
 ONETEP_STRESS_CELL = re.compile(
-    r"(?i)^\s*stress_calculation:\s*cell\s*geometry\s*$"
+    r'(?i)^\s*stress_calculation:\s*cell\s*geometry\s*$'
 )
 
 
@@ -75,10 +75,10 @@ def get_onetep_keywords(path):
     # If there is an include file, the entire
     # file keyword's will be included in the dict
     # and the include_file keyword will be deleted
-    if "include_file" in results["keywords"]:
-        warnings.warn("include_file will be deleted from the dict")
-        del results["keywords"]["include_file"]
-    return results["keywords"]
+    if 'include_file' in results['keywords']:
+        warnings.warn('include_file will be deleted from the dict')
+        del results['keywords']['include_file']
+    return results['keywords']
 
 
 def read_onetep_in(fd, **kwargs):
@@ -128,7 +128,7 @@ def read_onetep_in(fd, **kwargs):
         """
         new_lines = []
         for line in lines:
-            sep = re.split(r"[!#]", line.strip())[0]
+            sep = re.split(r'[!#]', line.strip())[0]
             if sep:
                 new_lines.append(sep)
         return new_lines
@@ -149,109 +149,109 @@ def read_onetep_in(fd, **kwargs):
     # Main loop reading the input
     for n, line in enumerate(fdi_lines):
         line_lower = line.lower()
-        if re.search(r"^\s*%block", line_lower):
+        if re.search(r'^\s*%block', line_lower):
             block_start = n + 1
-            if re.search(r"lattice_cart$", line_lower):
-                if re.search(r"^\s*ang\s*$", fdi_lines[block_start]):
-                    cell = np.loadtxt(fdi_lines[n + 2: n + 5])
+            if re.search(r'lattice_cart$', line_lower):
+                if re.search(r'^\s*ang\s*$', fdi_lines[block_start]):
+                    cell = np.loadtxt(fdi_lines[n + 2 : n + 5])
                 else:
-                    cell = np.loadtxt(fdi_lines[n + 1: n + 4])
+                    cell = np.loadtxt(fdi_lines[n + 1 : n + 4])
                     cell *= Bohr
 
         if not block_start:
-            if "devel_code" in line_lower:
-                warnings.warn("devel_code is not supported")
+            if 'devel_code' in line_lower:
+                warnings.warn('devel_code is not supported')
                 continue
             # Splits line on any valid onetep separator
-            sep = re.split(r"[:=\s]+", line)
-            keywords[sep[0]] = " ".join(sep[1:])
+            sep = re.split(r'[:=\s]+', line)
+            keywords[sep[0]] = ' '.join(sep[1:])
             # If include_file is used, we open the included file
             # and insert it in the current fdi_lines...
             # ONETEP does not work with cascade
             # and this SHOULD NOT work with cascade
-            if re.search(r"^\s*include_file$", sep[0]):
-                name = sep[1].replace("'", "")
-                name = name.replace('"', "")
+            if re.search(r'^\s*include_file$', sep[0]):
+                name = sep[1].replace("'", '')
+                name = name.replace('"', '')
                 new_path = fd_parent / name
                 for path in include_files:
                     if new_path.samefile(path):
-                        raise ValueError("invalid/recursive include_file")
+                        raise ValueError('invalid/recursive include_file')
                 new_fd = open(new_path)
                 new_lines = new_fd.readlines()
                 new_lines = clean_lines(new_lines)
                 for include_line in new_lines:
-                    sep = re.split(r"[:=\s]+", include_line)
-                    if re.search(r"^\s*include_file$", sep[0]):
-                        raise ValueError("nested include_file")
+                    sep = re.split(r'[:=\s]+', include_line)
+                    if re.search(r'^\s*include_file$', sep[0]):
+                        raise ValueError('nested include_file')
                 fdi_lines[:] = (
-                    fdi_lines[: n + 1] + new_lines + fdi_lines[n + 1:]
+                    fdi_lines[: n + 1] + new_lines + fdi_lines[n + 1 :]
                 )
                 include_files.append(new_path)
                 continue
 
-        if re.search(r"^\s*%endblock", line_lower):
-            if re.search(r"\s*positions_", line_lower):
-                head = re.search(r"(?i)^\s*(\S*)\s*$", fdi_lines[block_start])
-                head = head.group(1).lower() if head else ""
-                conv = 1 if head == "ang" else units["Bohr"]
+        if re.search(r'^\s*%endblock', line_lower):
+            if re.search(r'\s*positions_', line_lower):
+                head = re.search(r'(?i)^\s*(\S*)\s*$', fdi_lines[block_start])
+                head = head.group(1).lower() if head else ''
+                conv = 1 if head == 'ang' else units['Bohr']
                 # Skip one line if head is True
-                to_read = fdi_lines[block_start + int(bool(head)): n]
+                to_read = fdi_lines[block_start + int(bool(head)) : n]
                 positions = np.loadtxt(to_read, usecols=(1, 2, 3))
                 positions *= conv
-                symbols = np.loadtxt(to_read, usecols=(0), dtype="str")
-                if re.search(r".*frac$", line_lower):
+                symbols = np.loadtxt(to_read, usecols=(0), dtype='str')
+                if re.search(r'.*frac$', line_lower):
                     fractional = True
-            elif re.search(r"^\s*%endblock\s*species$", line_lower):
+            elif re.search(r'^\s*%endblock\s*species$', line_lower):
                 els = fdi_lines[block_start:n]
                 species = {}
                 for el in els:
                     sep = el.split()
                     species[sep[0]] = sep[1]
                 to_read = [i.strip() for i in fdi_lines[block_start:n]]
-                keywords["species"] = to_read
-            elif re.search(r"lattice_cart$", line_lower):
+                keywords['species'] = to_read
+            elif re.search(r'lattice_cart$', line_lower):
                 pass
             else:
                 to_read = [i.strip() for i in fdi_lines[block_start:n]]
-                block_title = line_lower.replace("%endblock", "").strip()
+                block_title = line_lower.replace('%endblock', '').strip()
                 keywords[block_title] = to_read
             block_start = 0
 
     # We don't need a fully valid onetep
     # input to read the keywords, just
     # the keywords
-    if kwargs.get("only_keywords", False):
-        return {"keywords": keywords}
+    if kwargs.get('only_keywords', False):
+        return {'keywords': keywords}
     # Necessary if we have only one atom
     # Check if the cell is valid (3D)
     if not cell.any(axis=1).all():
-        raise ValueError("invalid cell specified")
+        raise ValueError('invalid cell specified')
 
     if positions is False:
-        raise ValueError("invalid position specified")
+        raise ValueError('invalid position specified')
 
     if symbols is False:
-        raise ValueError("no symbols found")
+        raise ValueError('no symbols found')
 
     positions = positions.reshape(-1, 3)
     symbols = symbols.reshape(-1)
     tags = []
-    info = {"onetep_species": []}
+    info = {'onetep_species': []}
     for symbol in symbols:
-        label = symbol.replace(species[symbol], "")
+        label = symbol.replace(species[symbol], '')
         if label.isdigit():
             tags.append(int(label))
         else:
             tags.append(0)
-        info["onetep_species"].append(symbol)
+        info['onetep_species'].append(symbol)
     atoms = Atoms(
         [species[i] for i in symbols], cell=cell, pbc=True, tags=tags, info=info
     )
     if fractional:
-        atoms.set_scaled_positions(positions / units["Bohr"])
+        atoms.set_scaled_positions(positions / units['Bohr'])
     else:
         atoms.set_positions(positions)
-    results = {"atoms": atoms, "keywords": keywords}
+    results = {'atoms': atoms, 'keywords': keywords}
     return results
 
 
@@ -259,14 +259,14 @@ def write_onetep_in(
     fd,
     atoms,
     edft=False,
-    xc="PBE",
+    xc='PBE',
     ngwf_count=-1,
     ngwf_radius=9.0,
     keywords={},
     pseudopotentials={},
-    pseudo_path=".",
+    pseudo_path='.',
     pseudo_suffix=None,
-    **kwargs
+    **kwargs,
 ):
     """
     Write a single ONETEP input.
@@ -340,15 +340,15 @@ def write_onetep_in(
         pseudopotentials in pseudo_path.
     """
 
-    label = kwargs.get("label", "onetep")
+    label = kwargs.get('label', 'onetep')
     try:
-        directory = kwargs.get("directory", Path(dirname(fd.name)))
+        directory = kwargs.get('directory', Path(dirname(fd.name)))
     except AttributeError:
-        directory = "."
-    autorestart = kwargs.get("autorestart", False)
+        directory = '.'
+    autorestart = kwargs.get('autorestart', False)
     elements = np.array(atoms.symbols)
     tags = np.array(atoms.get_tags())
-    species_maybe = atoms.info.get("onetep_species", False)
+    species_maybe = atoms.info.get('onetep_species', False)
     #  We look if the atom.info contains onetep species information
     # If it does, we use it, as it might contains character
     #  which are not allowed in ase tags, if not we fall back
@@ -359,7 +359,7 @@ def write_onetep_in(
         else:
             species = elements
     else:
-        formatted_tags = np.array(["" if i == 0 else str(i) for i in tags])
+        formatted_tags = np.array(['' if i == 0 else str(i) for i in tags])
         species = np.char.add(elements, formatted_tags)
     numbers = np.array(atoms.numbers)
     tmp = np.argsort(species)
@@ -383,7 +383,7 @@ def write_onetep_in(
     elif isinstance(ngwf_count, dict):
         pass
     else:
-        raise TypeError("ngwf_count can only be int|list|dict")
+        raise TypeError('ngwf_count can only be int|list|dict')
 
     if isinstance(ngwf_radius, float):
         ngwf_radius = dict(zip(u_species, [ngwf_radius] * n_sp))
@@ -392,77 +392,77 @@ def write_onetep_in(
     elif isinstance(ngwf_radius, dict):
         pass
     else:
-        raise TypeError("ngwf_radius can only be float|list|dict")
+        raise TypeError('ngwf_radius can only be float|list|dict')
 
-    pp_files = re.sub("'|\"", "", keywords.get("pseudo_path", pseudo_path))
-    pp_files = Path(pp_files).glob("*")
+    pp_files = re.sub('\'|"', '', keywords.get('pseudo_path', pseudo_path))
+    pp_files = Path(pp_files).glob('*')
     pp_files = [i for i in pp_files if i.is_file()]
 
     if pseudo_suffix:
         common_suffix = [pseudo_suffix]
     else:
-        common_suffix = [".usp", ".recpot", ".upf", ".paw", ".psp", ".pspnc"]
+        common_suffix = ['.usp', '.recpot', '.upf', '.paw', '.psp', '.pspnc']
 
-    if keywords.get("species_pot", False):
-        pp_list = keywords["species_pot"]
+    if keywords.get('species_pot', False):
+        pp_list = keywords['species_pot']
     elif isinstance(pseudopotentials, dict):
         pp_list = []
         for idx, el in enumerate(u_species):
             if el in pseudopotentials:
-                pp_list.append(f"{el} {pseudopotentials[el]}")
+                pp_list.append(f'{el} {pseudopotentials[el]}')
             else:
                 for i in pp_files:
-                    reg_el_candidate = re.split(r"[-_.:= ]+", i.stem)[0]
+                    reg_el_candidate = re.split(r'[-_.:= ]+', i.stem)[0]
                     if (
                         elements[idx] == reg_el_candidate.title()
                         and i.suffix.lower() in common_suffix
                     ):
-                        pp_list.append(f"{el} {i.name}")
+                        pp_list.append(f'{el} {i.name}')
     else:
-        raise TypeError("pseudopotentials object can only be dict")
+        raise TypeError('pseudopotentials object can only be dict')
 
     default_species = []
     for idx, el in enumerate(u_species):
-        tmp = ""
-        tmp += u_species[idx] + " " + elements[idx] + " "
-        tmp += str(numbers[idx]) + " "
+        tmp = ''
+        tmp += u_species[idx] + ' ' + elements[idx] + ' '
+        tmp += str(numbers[idx]) + ' '
         try:
-            tmp += str(ngwf_count[el]) + " "
+            tmp += str(ngwf_count[el]) + ' '
         except KeyError:
-            tmp += str(ngwf_count[elements[idx]]) + " "
+            tmp += str(ngwf_count[elements[idx]]) + ' '
         try:
             tmp += str(ngwf_radius[el])
         except KeyError:
             tmp += str(ngwf_radius[elements[idx]])
         default_species.append(tmp)
 
-    positions_abs = ["ang"]
+    positions_abs = ['ang']
     for s, p in zip(species, atoms.get_positions()):
-        line = "{s:>5} {0:>12.6f} {1:>12.6f} {2:>12.6f}".format(s=s, *p)
+        line = '{s:>5} {0:>12.6f} {1:>12.6f} {2:>12.6f}'.format(s=s, *p)
         positions_abs.append(line)
 
-    lattice_cart = ["ang"]
+    lattice_cart = ['ang']
     for axis in atoms.get_cell():
-        line = "{:>16.8f} {:>16.8f} {:>16.8f}".format(*axis)
+        line = '{:>16.8f} {:>16.8f} {:>16.8f}'.format(*axis)
         lattice_cart.append(line)
 
     # Default keywords if not provided by the user,
     # most of them are ONETEP default, except write_forces
     # which is always turned on.
     default_keywords = {
-        "xc_functional": xc,
-        "edft": edft,
-        "cutoff_energy": 20,
-        "paw": False,
-        "task": "singlepoint",
-        "output_detail": "normal",
-        "species": default_species,
-        "pseudo_path": pseudo_path,
-        "species_pot": pp_list,
-        "positions_abs": positions_abs,
-        "lattice_cart": lattice_cart,
-        "write_forces": True,
-        "forces_output_detail": "verbose",
+        'xc_functional': xc,
+        'edft': edft,
+        'cutoff_energy': 20,
+        'paw': False,
+        'task': 'singlepoint',
+        'output_detail': 'normal',
+        'species': default_species,
+        'pseudo_path': pseudo_path,
+        'species_pot': pp_list,
+        'positions_abs': positions_abs,
+        'lattice_cart': lattice_cart,
+        'write_forces': True,
+        'forces_output_detail': 'verbose',
     }
 
     # Main loop, fill the keyword dictionary
@@ -475,11 +475,11 @@ def write_onetep_in(
     # If autorestart is True, we look for restart files,
     # and turn on relevant keywords...
     if autorestart:
-        keywords["read_denskern"] = isfile(directory / (label + ".dkn"))
-        keywords["read_tightbox_ngwfs"] = isfile(
-            directory / (label + ".tightbox_ngwfs")
+        keywords['read_denskern'] = isfile(directory / (label + '.dkn'))
+        keywords['read_tightbox_ngwfs'] = isfile(
+            directory / (label + '.tightbox_ngwfs')
         )
-        keywords["read_hamiltonian"] = isfile(directory / (label + ".ham"))
+        keywords['read_hamiltonian'] = isfile(directory / (label + '.ham'))
 
     # If not EDFT, hamiltonian is irrelevant.
     # print(keywords.get('edft', False))
@@ -494,51 +494,51 @@ def write_onetep_in(
     for key, value in keywords.items():
         if isinstance(value, (list, np.ndarray)):
             if not all(isinstance(_, str) for _ in value):
-                raise TypeError("list values for blocks must be strings only")
-            block_lines.append(("\n%block " + key).upper())
+                raise TypeError('list values for blocks must be strings only')
+            block_lines.append(('\n%block ' + key).upper())
             block_lines.extend(value)
-            block_lines.append(("%endblock " + key).upper())
+            block_lines.append(('%endblock ' + key).upper())
         elif isinstance(value, bool):
-            lines.append(str(key) + " : " + str(value)[0])
+            lines.append(str(key) + ' : ' + str(value)[0])
         elif isinstance(value, (str, int, float)):
-            lines.append(str(key) + " : " + str(value))
+            lines.append(str(key) + ' : ' + str(value))
         else:
-            raise TypeError("keyword values must be list|str|bool")
+            raise TypeError('keyword values must be list|str|bool')
     input_header = (
-        "!"
-        + "-" * 78
-        + "!\n"
-        + "!"
-        + "-" * 33
-        + " INPUT FILE "
-        + "-" * 33
-        + "!\n"
-        + "!"
-        + "-" * 78
-        + "!\n\n"
+        '!'
+        + '-' * 78
+        + '!\n'
+        + '!'
+        + '-' * 33
+        + ' INPUT FILE '
+        + '-' * 33
+        + '!\n'
+        + '!'
+        + '-' * 78
+        + '!\n\n'
     )
 
     input_footer = (
-        "\n!"
-        + "-" * 78
-        + "!\n"
-        + "!"
-        + "-" * 32
-        + " END OF INPUT "
-        + "-" * 32
-        + "!\n"
-        + "!"
-        + "-" * 78
-        + "!"
+        '\n!'
+        + '-' * 78
+        + '!\n'
+        + '!'
+        + '-' * 32
+        + ' END OF INPUT '
+        + '-' * 32
+        + '!\n'
+        + '!'
+        + '-' * 78
+        + '!'
     )
 
     fd.write(input_header)
-    fd.writelines(line + "\n" for line in lines)
-    fd.writelines(b_line + "\n" for b_line in block_lines)
+    fd.writelines(line + '\n' for line in lines)
+    fd.writelines(b_line + '\n' for b_line in block_lines)
 
-    if "devel_code" in kwargs:
-        warnings.warn("writing devel code as it is, at the end of the file")
-        fd.writelines("\n" + line for line in kwargs["devel_code"])
+    if 'devel_code' in kwargs:
+        warnings.warn('writing devel code as it is, at the end of the file')
+        fd.writelines('\n' + line for line in kwargs['devel_code'])
 
     fd.write(input_footer)
 
@@ -549,7 +549,7 @@ def read_onetep_out(fd, index=-1, improv
 
     !!!
     This function will be used by ASE when performing
-    various workflows (Opt, NEB...)
+    various workflows (opt, NEB...)
     !!!
 
     Parameters
@@ -571,7 +571,7 @@ def read_onetep_out(fd, index=-1, improv
     fdo_lines = fd.readlines()
     n_lines = len(fdo_lines)
 
-    freg = re.compile(r"-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+\-]?\d+)?")
+    freg = re.compile(r'-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+\-]?\d+)?')
 
     # Used to store index of important elements
     output = {
@@ -607,7 +607,7 @@ def read_onetep_out(fd, index=-1, improv
     ]
 
     # Find all matches append them to the dictionary
-    breg = "|".join([i.pattern.replace("(?i)", "") for i in output.keys()])
+    breg = '|'.join([i.pattern.replace('(?i)', '') for i in output.keys()])
     prematch = {}
 
     for idx, line in enumerate(fdo_lines):
@@ -648,7 +648,7 @@ def read_onetep_out(fd, index=-1, improv
     core_keywords = np.sort(core_keywords)
 
     i_first_positions = output[ONETEP_FIRST_POSITION]
-    is_frac_positions = [i for i in i_first_positions if "FRAC" in fdo_lines[i]]
+    is_frac_positions = [i for i in i_first_positions if 'FRAC' in fdo_lines[i]]
 
     # In onetep species can have arbritary names,
     # We want to map them to real element names
@@ -693,15 +693,15 @@ def read_onetep_out(fd, index=-1, improv
             if past < idx < future:
                 if past in onetep_start:
                     if future in ibfgs_start or future in ibfgs_resume:
-                        return "resume"
+                        return 'resume'
                     continue
                 # Are we in start or resume or improve
                 if past in ibfgs_start:
-                    return "start"
+                    return 'start'
                 elif past in ibfgs_resume:
-                    return "resume"
+                    return 'resume'
                 elif past in ibfgs_improve:
-                    return "improve"
+                    return 'improve'
 
         return False
 
@@ -737,12 +737,12 @@ def read_onetep_out(fd, index=-1, improv
         if has_bfgs:
             which_bfgs = where_in_bfgs(past)
 
-            if which_bfgs == "resume":
+            if which_bfgs == 'resume':
                 to_del.append(idx)
                 continue
 
             if not improving:
-                if which_bfgs == "improve":
+                if which_bfgs == 'improve':
                     to_del.append(idx)
                     continue
 
@@ -761,7 +761,7 @@ def read_onetep_out(fd, index=-1, improv
 
     # Bunch of methods to grep properties from output.
     def parse_cell(idx):
-        a, b, c = np.loadtxt([fdo_lines[idx + 2]]) * units["Bohr"]
+        a, b, c = np.loadtxt([fdo_lines[idx + 2]]) * units['Bohr']
         al, be, ga = np.loadtxt([fdo_lines[idx + 4]])
         cell = Cell.fromcellpar([a, b, c, al, be, ga])
         return np.array(cell)
@@ -772,7 +772,7 @@ def read_onetep_out(fd, index=-1, improv
         while idx + n < len(fdo_lines):
             if not fdo_lines[idx + n].strip():
                 tmp_charges = np.loadtxt(
-                    fdo_lines[idx + offset: idx + n - 1], usecols=3
+                    fdo_lines[idx + offset : idx + n - 1], usecols=3
                 )
                 return np.reshape(tmp_charges, -1)
             n += 1
@@ -786,9 +786,9 @@ def read_onetep_out(fd, index=-1, improv
     def parse_energy(idx):
         n = 0
         while idx + n < len(fdo_lines):
-            if re.search(r"^\s*\|\s*Total\s*:.*\|\s*$", fdo_lines[idx + n]):
+            if re.search(r'^\s*\|\s*Total\s*:.*\|\s*$', fdo_lines[idx + n]):
                 energy_str = re.search(freg, fdo_lines[idx + n]).group(0)
-                return float(energy_str) * units["Hartree"]
+                return float(energy_str) * units['Hartree']
             n += 1
         return None
 
@@ -796,14 +796,14 @@ def read_onetep_out(fd, index=-1, improv
         n = 0
         fermi_levels = None
         while idx + n < len(fdo_lines):
-            if "Fermi_level" in fdo_lines[idx + n]:
-                tmp = "\n".join(fdo_lines[idx + n: idx + n + 1])
+            if 'Fermi_level' in fdo_lines[idx + n]:
+                tmp = '\n'.join(fdo_lines[idx + n : idx + n + 1])
                 fermi_level = re.findall(freg, tmp)
                 fermi_levels = [
-                    float(i) * units["Hartree"] for i in fermi_level
+                    float(i) * units['Hartree'] for i in fermi_level
                 ]
             if re.search(
-                r"^\s*<{5}\s*CALCULATION\s*SUMMARY\s*>{5}\s*$",
+                r'^\s*<{5}\s*CALCULATION\s*SUMMARY\s*>{5}\s*$',
                 fdo_lines[idx + n],
             ):
                 return fermi_levels
@@ -819,12 +819,12 @@ def read_onetep_out(fd, index=-1, improv
             ):
                 offset += 1
             if re.search(
-                r"(?i)^\s*%ENDBLOCK\s*LATTICE"
-                r"\s*_?\s*CART\s*:?\s*([*#!].*)?$",
+                r'(?i)^\s*%ENDBLOCK\s*LATTICE'
+                r'\s*_?\s*CART\s*:?\s*([*#!].*)?$',
                 fdo_lines[idx + n],
             ):
-                cell = np.loadtxt(fdo_lines[idx + offset: idx + n])
-                return cell if offset == 2 else cell * units["Bohr"]
+                cell = np.loadtxt(fdo_lines[idx + offset : idx + n])
+                return cell if offset == 2 else cell * units['Bohr']
             n += 1
         return None
 
@@ -836,13 +836,13 @@ def read_onetep_out(fd, index=-1, improv
                 r'(?i)^\s*"?\s*ang\s*"?\s*([*#!].*)?$', fdo_lines[idx + n]
             ):
                 offset += 1
-            if re.search(r"^\s*%ENDBLOCK\s*POSITIONS_", fdo_lines[idx + n]):
-                if "FRAC" in fdo_lines[idx + n]:
+            if re.search(r'^\s*%ENDBLOCK\s*POSITIONS_', fdo_lines[idx + n]):
+                if 'FRAC' in fdo_lines[idx + n]:
                     conv_factor = 1
                 else:
-                    conv_factor = units["Bohr"]
+                    conv_factor = units['Bohr']
                 tmp = np.loadtxt(
-                    fdo_lines[idx + offset: idx + n], dtype="str"
+                    fdo_lines[idx + offset : idx + n], dtype='str'
                 ).reshape(-1, 4)
                 els = np.char.array(tmp[:, 0])
                 if offset == 2:
@@ -859,7 +859,7 @@ def read_onetep_out(fd, index=-1, improv
                     )
                     atoms = Atoms(real_elements, pos)
                     atoms.set_tags(tags)
-                    atoms.info["onetep_species"] = list(els)
+                    atoms.info['onetep_species'] = list(els)
                 return atoms
             n += 1
         return None
@@ -867,13 +867,13 @@ def read_onetep_out(fd, index=-1, improv
     def parse_force(idx):
         n = 0
         while idx + n < len(fdo_lines):
-            if re.search(r"(?i)^\s*\*\s*TOTAL:.*\*\s*$", fdo_lines[idx + n]):
+            if re.search(r'(?i)^\s*\*\s*TOTAL:.*\*\s*$', fdo_lines[idx + n]):
                 tmp = np.loadtxt(
-                    fdo_lines[idx + 6: idx + n - 2],
+                    fdo_lines[idx + 6 : idx + n - 2],
                     dtype=np.float64,
                     usecols=(3, 4, 5),
                 )
-                return tmp * units["Hartree"] / units["Bohr"]
+                return tmp * units['Hartree'] / units['Bohr']
             n += 1
         return None
 
@@ -882,16 +882,16 @@ def read_onetep_out(fd, index=-1, improv
         offset = 7
         stop = 0
         while idx + n < len(fdo_lines):
-            if re.search(r"^\s*x{60,}\s*$", fdo_lines[idx + n]):
+            if re.search(r'^\s*x{60,}\s*$', fdo_lines[idx + n]):
                 stop += 1
             if stop == 2:
                 tmp = np.loadtxt(
-                    fdo_lines[idx + offset: idx + n],
-                    dtype="str",
+                    fdo_lines[idx + offset : idx + n],
+                    dtype='str',
                     usecols=(1, 3, 4, 5),
                 )
                 els = np.char.array(tmp[:, 0])
-                pos = tmp[:, 1:].astype(np.float64) * units["Bohr"]
+                pos = tmp[:, 1:].astype(np.float64) * units['Bohr']
                 try:
                     atoms = Atoms(els, pos)
                 # ASE doesn't recognize names used in ONETEP
@@ -900,7 +900,7 @@ def read_onetep_out(fd, index=-1, improv
                     tags, real_elements = find_correct_species(els, idx)
                     atoms = Atoms(real_elements, pos)
                     atoms.set_tags(tags)
-                    atoms.info["onetep_species"] = list(els)
+                    atoms.info['onetep_species'] = list(els)
                 return atoms
             n += 1
         return None
@@ -911,12 +911,12 @@ def read_onetep_out(fd, index=-1, improv
         while idx + n < len(fdo_lines):
             sep = fdo_lines[idx + n].split()
             if re.search(
-                r"(?i)^\s*%ENDBLOCK\s*SPECIES\s*:?\s*([*#!].*)?$",
+                r'(?i)^\s*%ENDBLOCK\s*SPECIES\s*:?\s*([*#!].*)?$',
                 fdo_lines[idx + n],
             ):
                 return element_map
             to_skip = re.search(
-                r"(?i)^\s*(ang|bohr)\s*([*#!].*)?$", fdo_lines[idx + n]
+                r'(?i)^\s*(ang|bohr)\s*([*#!].*)?$', fdo_lines[idx + n]
             )
             if not to_skip:
                 element_map[sep[0]] = sep[1]
@@ -931,7 +931,7 @@ def read_onetep_out(fd, index=-1, improv
                 # If no spin is present we return None
                 try:
                     tmp_spins = np.loadtxt(
-                        fdo_lines[idx + offset: idx + n - 1], usecols=4
+                        fdo_lines[idx + offset : idx + n - 1], usecols=4
                     )
                     return np.reshape(tmp_spins, -1)
                 except ValueError:
@@ -954,7 +954,7 @@ def read_onetep_out(fd, index=-1, improv
         elements_map = real_species[closest_species]
         for el in els:
             real_elements.append(elements_map[el])
-            tag_maybe = el.replace(elements_map[el], "")
+            tag_maybe = el.replace(elements_map[el], '')
             if tag_maybe.isdigit():
                 tags.append(int(tag_maybe))
             else:
diff -pruN 3.24.0-1/ase/io/opls.py 3.26.0-1/ase/io/opls.py
--- 3.24.0-1/ase/io/opls.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/opls.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import time
 
 import numpy as np
diff -pruN 3.24.0-1/ase/io/orca.py 3.26.0-1/ase/io/orca.py
--- 3.24.0-1/ase/io/orca.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/orca.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,15 +1,18 @@
 import re
+import warnings
+from collections.abc import Iterable, Iterator
 from io import StringIO
 from pathlib import Path
-from typing import List, Optional
+from typing import List, Optional, Sequence
 
 import numpy as np
 
-from ase.io import read
+from ase import Atoms
+from ase.calculators.calculator import PropertyNotImplementedError
+from ase.calculators.singlepoint import SinglePointDFTCalculator
+from ase.io import ParseError, read
 from ase.units import Bohr, Hartree
-from ase.utils import reader, writer
-
-# Made from NWChem interface
+from ase.utils import deprecated, reader, writer
 
 
 @reader
@@ -23,9 +26,9 @@ def read_geom_orcainp(fd):
         if line[1:].startswith('xyz '):
             startline = index + 1
             stopline = -1
-        elif (line.startswith('end') and stopline == -1):
+        elif line.startswith('end') and stopline == -1:
             stopline = index
-        elif (line.startswith('*') and stopline == -1):
+        elif line.startswith('*') and stopline == -1:
             stopline = index
     # Format and send to read_xyz.
     xyz_text = '%i\n' % (stopline - startline)
@@ -33,7 +36,7 @@ def read_geom_orcainp(fd):
     for line in lines[startline:stopline]:
         xyz_text += line
     atoms = read(StringIO(xyz_text), format='xyz')
-    atoms.set_cell((0., 0., 0.))  # no unit cell defined
+    atoms.set_cell((0.0, 0.0, 0.0))  # no unit cell defined
 
     return atoms
 
@@ -41,13 +44,13 @@ def read_geom_orcainp(fd):
 @writer
 def write_orca(fd, atoms, params):
     # conventional filename: '<name>.inp'
-    fd.write(f"! {params['orcasimpleinput']} \n")
-    fd.write(f"{params['orcablocks']} \n")
+    fd.write(f'! {params["orcasimpleinput"]} \n')
+    fd.write(f'{params["orcablocks"]} \n')
 
     if 'coords' not in params['orcablocks']:
         fd.write('*xyz')
-        fd.write(" %d" % params['charge'])
-        fd.write(" %d \n" % params['mult'])
+        fd.write(' %d' % params['charge'])
+        fd.write(' %d \n' % params['mult'])
         for atom in atoms:
             if atom.tag == 71:  # 71 is ascii G (Ghost)
                 symbol = atom.symbol + ' : '
@@ -56,11 +59,11 @@ def write_orca(fd, atoms, params):
             fd.write(
                 symbol
                 + str(atom.position[0])
-                + " "
+                + ' '
                 + str(atom.position[1])
-                + " "
+                + ' '
                 + str(atom.position[2])
-                + "\n"
+                + '\n'
             )
         fd.write('*\n')
 
@@ -79,7 +82,7 @@ def read_energy(lines: List[str]) -> Opt
     energy = None
     for line in lines:
         if 'FINAL SINGLE POINT ENERGY' in line:
-            if "Wavefunction not fully converged" in line:
+            if 'Wavefunction not fully converged' in line:
                 energy = float('nan')
             else:
                 energy = float(line.split()[-1])
@@ -89,7 +92,7 @@ def read_energy(lines: List[str]) -> Opt
 
 
 def read_center_of_mass(lines: List[str]) -> Optional[np.ndarray]:
-    """ Scan through text for the center of mass """
+    """Scan through text for the center of mass"""
     # Example:
     # 'The origin for moment calculation is the CENTER OF MASS  =
     # ( 0.002150, -0.296255  0.086315)'
@@ -112,33 +115,171 @@ def read_dipole(lines: List[str]) -> Opt
     dipole = None
     for line in lines:
         if 'Total Dipole Moment' in line:
-            dipole = np.array([float(_) for _ in line.split()[-3:]])
-    if dipole is not None:
-        return dipole * Bohr  # Return the last match
-    return dipole
+            dipole = np.array([float(x) for x in line.split()[-3:]]) * Bohr
+    return dipole  # Return the last match
 
 
-@reader
-def read_orca_output(fd):
-    """ From the ORCA output file: Read Energy and dipole moment
-    in the frame of reference of the center of mass "
+def _read_atoms(lines: Sequence[str]) -> Atoms:
+    """Read atomic positions and symbols. Create Atoms object."""
+    line_start = -1
+    natoms = 0
+
+    for ll, line in enumerate(lines):
+        if 'Number of atoms' in line:
+            natoms = int(line.split()[4])
+        elif 'CARTESIAN COORDINATES (ANGSTROEM)' in line:
+            line_start = ll + 2
+
+    # Check if atoms present and if their number is given.
+    if line_start == -1:
+        raise ParseError(
+            'No information about the structure in the ORCA output file.'
+        )
+    elif natoms == 0:
+        raise ParseError(
+            'No information about number of atoms in the ORCA output file.'
+        )
+
+    positions = np.zeros((natoms, 3))
+    symbols = [''] * natoms
+
+    for ll, line in enumerate(lines[line_start : (line_start + natoms)]):
+        inp = line.split()
+        positions[ll, :] = [float(pos) for pos in inp[1:4]]
+        symbols[ll] = inp[0]
+
+    atoms = Atoms(symbols=symbols, positions=positions)
+    atoms.set_pbc([False, False, False])
+
+    return atoms
+
+
+def read_forces(lines: List[str]) -> Optional[np.ndarray]:
+    """Read forces from output file if available. Else return None.
+
+    Taking the forces from the output files (instead of the engrad-file) to
+    be more general. The forces can be present in general output even if
+    the engrad file is not there.
+
+    Note: If more than one geometry relaxation step is available,
+          forces do not always exist for the first step. In this case, for
+          the first step an array of None will be returned. The following
+          relaxation steps will then have forces available.
     """
-    lines = fd.readlines()
+    line_start = -1
+    natoms = 0
+    record_gradient = True
+
+    for ll, line in enumerate(lines):
+        if 'Number of atoms' in line:
+            natoms = int(line.split()[4])
+        # Read in only first set of forces for each chunk
+        # (Excited state calculations can have several sets of
+        # forces per chunk)
+        elif 'CARTESIAN GRADIENT' in line and record_gradient:
+            line_start = ll + 3
+            record_gradient = False
+
+    # Check if number of atoms is available.
+    if natoms == 0:
+        raise ParseError(
+            'No information about number of atoms in the ORCA output file.'
+        )
+
+    # Forces are not always available. If not available, return None.
+    if line_start == -1:
+        return None
+
+    forces = np.zeros((natoms, 3))
+
+    for ll, line in enumerate(lines[line_start : (line_start + natoms)]):
+        inp = line.split()
+        forces[ll, :] = [float(pos) for pos in inp[3:6]]
 
-    energy = read_energy(lines)
-    charge = read_charge(lines)
-    com = read_center_of_mass(lines)
-    dipole = read_dipole(lines)
+    forces *= -Hartree / Bohr
+    return forces
 
-    results = {}
-    results['energy'] = energy
-    results['free_energy'] = energy
 
-    if com is not None and dipole is not None:
-        dipole = dipole + com * charge
-        results['dipole'] = dipole
+def get_chunks(lines: Iterable[str]) -> Iterator[list[str]]:
+    """Separate out the chunks for each geometry relaxation step."""
+    finished = False
+    relaxation_finished = False
+    relaxation = False
+
+    chunk_endings = [
+        'ORCA TERMINATED NORMALLY',
+        'ORCA GEOMETRY RELAXATION STEP',
+    ]
+    chunk_lines = []
+    for line in lines:
+        # Assemble chunks
+        if any([ending in line for ending in chunk_endings]):
+            chunk_lines.append(line)
+            yield chunk_lines
+            chunk_lines = []
+        else:
+            chunk_lines.append(line)
+
+        if 'ORCA TERMINATED NORMALLY' in line:
+            finished = True
+
+        if 'THE OPTIMIZATION HAS CONVERGED' in line:
+            relaxation_finished = True
+
+        # Check if calculation is an optimization.
+        if 'ORCA SCF GRADIENT CALCULATION' in line:
+            relaxation = True
+
+    # Give error if calculation not finished for single-point calculations.
+    if not finished and not relaxation:
+        raise ParseError('Error: Calculation did not finish!')
+    # Give warning if calculation not finished for geometry optimizations.
+    elif not finished and relaxation:
+        warnings.warn('Calculation did not finish!')
+    # Calculation may have finished, but relaxation may have not.
+    elif not relaxation_finished and relaxation:
+        warnings.warn('Geometry optimization did not converge!')
 
-    return results
+
+@reader
+def read_orca_output(fd, index=slice(None)):
+    """From the ORCA output file: Read Energy, positions, forces
+       and dipole moment.
+
+    Create separated atoms object for each geometry frame through
+    parsing the output file in chunks.
+    """
+    images = []
+
+    # Iterate over chunks and create a separate atoms object for each
+    for chunk in get_chunks(fd):
+        energy = read_energy(chunk)
+        atoms = _read_atoms(chunk)
+        forces = read_forces(chunk)
+        dipole = read_dipole(chunk)
+        charge = read_charge(chunk)
+        com = read_center_of_mass(chunk)
+
+        # Correct dipole moment for centre-of-mass
+        if com is not None and dipole is not None:
+            dipole = dipole + com * charge
+
+        atoms.calc = SinglePointDFTCalculator(
+            atoms,
+            energy=energy,
+            free_energy=energy,
+            forces=forces,
+            # stress=self.stress,
+            # stresses=self.stresses,
+            # magmom=self.magmom,
+            dipole=dipole,
+            # dielectric_tensor=self.dielectric_tensor,
+            # polarization=self.polarization,
+        )
+        # collect images
+        images.append(atoms)
+
+    return images[index]
 
 
 @reader
@@ -153,7 +294,7 @@ def read_orca_engrad(fd):
             gradients = []
             tempgrad = []
             continue
-        if getgrad and "#" not in line:
+        if getgrad and '#' not in line:
             grad = line.split()[-1]
             tempgrad.append(float(grad))
             if len(tempgrad) == 3:
@@ -166,10 +307,31 @@ def read_orca_engrad(fd):
     return forces
 
 
+@deprecated(
+    'Please use ase.io.read instead of read_orca_outputs, e.g.,\n'
+    'from ase.io import read \n'
+    'atoms = read("orca.out")',
+    DeprecationWarning,
+)
 def read_orca_outputs(directory, stdout_path):
+    """Reproduces old functionality of reading energy, forces etc
+       directly from output without creation of atoms object.
+       This is kept to ensure backwards compatability
+    .. deprecated:: 3.24.0
+       Use of read_orca_outputs is deprected, please
+       process ORCA output by using ase.io.read
+       e.g., read('orca.out')"
+    """
     stdout_path = Path(stdout_path)
+    atoms = read_orca_output(stdout_path, index=-1)
     results = {}
-    results.update(read_orca_output(stdout_path))
+    results['energy'] = atoms.get_total_energy()
+    results['free_energy'] = atoms.get_total_energy()
+
+    try:
+        results['dipole'] = atoms.get_dipole_moment()
+    except PropertyNotImplementedError:
+        pass
 
     # Does engrad always exist? - No!
     # Will there be other files -No -> We should just take engrad
@@ -178,4 +340,8 @@ def read_orca_outputs(directory, stdout_
     engrad_path = stdout_path.with_suffix('.engrad')
     if engrad_path.is_file():
         results['forces'] = read_orca_engrad(engrad_path)
+        print("""Warning: If you are reading in an engrad file from a
+              geometry optimization, check very carefully.
+              ORCA does not by default supply the forces for the
+              converged geometry!""")
     return results
diff -pruN 3.24.0-1/ase/io/pickletrajectory.py 3.26.0-1/ase/io/pickletrajectory.py
--- 3.24.0-1/ase/io/pickletrajectory.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/pickletrajectory.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import collections
 import errno
 import os
diff -pruN 3.24.0-1/ase/io/png.py 3.26.0-1/ase/io/png.py
--- 3.24.0-1/ase/io/png.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/png.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.io.eps import EPS
diff -pruN 3.24.0-1/ase/io/pov.py 3.26.0-1/ase/io/pov.py
--- 3.24.0-1/ase/io/pov.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/pov.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Module for povray file format support.
 
@@ -269,7 +271,8 @@ class POVRAY:
         if canvas_width is None:
             if canvas_height is None:
                 self.canvas_width = min(self.image_width * 15, 640)
-                self.canvas_height = min(self.image_height * 15, 640)
+                # a guess should respect the aspect ratio
+                self.canvas_height = self.canvas_width / ratio
             else:
                 self.canvas_width = canvas_height * ratio
                 self.canvas_height = canvas_height
diff -pruN 3.24.0-1/ase/io/prismatic.py 3.26.0-1/ase/io/prismatic.py
--- 3.24.0-1/ase/io/prismatic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/prismatic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Module to read and write atoms in xtl file format for the prismatic and
 computem software.
 
diff -pruN 3.24.0-1/ase/io/proteindatabank.py 3.26.0-1/ase/io/proteindatabank.py
--- 3.24.0-1/ase/io/proteindatabank.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/proteindatabank.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Module to read and write atoms in PDB file format.
 
 See::
diff -pruN 3.24.0-1/ase/io/py.py 3.26.0-1/ase/io/py.py
--- 3.24.0-1/ase/io/py.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/py.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 
diff -pruN 3.24.0-1/ase/io/qbox.py 3.26.0-1/ase/io/qbox.py
--- 3.24.0-1/ase/io/qbox.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/qbox.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """This module contains functions to read from QBox output files"""
 
 import re
diff -pruN 3.24.0-1/ase/io/res.py 3.26.0-1/ase/io/res.py
--- 3.24.0-1/ase/io/res.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/res.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 SHELX (.res) input/output
 
diff -pruN 3.24.0-1/ase/io/rmc6f.py 3.26.0-1/ase/io/rmc6f.py
--- 3.24.0-1/ase/io/rmc6f.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/rmc6f.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 import time
 
@@ -414,7 +416,7 @@ def write_rmc6f(filename, atoms, order=N
 
     header_lines = [
         "(Version 6f format configuration file)",
-        "(Generated by ASE - Atomic Simulation Environment https://wiki.fysik.dtu.dk/ase/ )",  # noqa: E501
+        "(Generated by ASE - Atomic Simulation Environment https://ase-lib.org/ )",  # noqa: E501
         "Metadata date: " + time.strftime('%d-%m-%Y'),
         f"Number of types of atoms:   {len(atom_types)} ",
         f"Atom types present:          {atom_types_present}",
diff -pruN 3.24.0-1/ase/io/sdf.py 3.26.0-1/ase/io/sdf.py
--- 3.24.0-1/ase/io/sdf.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/sdf.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Reads chemical data in SDF format (wraps the molfile format).
 
 See https://en.wikipedia.org/wiki/Chemical_table_file#SDF
diff -pruN 3.24.0-1/ase/io/siesta.py 3.26.0-1/ase/io/siesta.py
--- 3.24.0-1/ase/io/siesta.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/siesta.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Helper functions for read_fdf."""
 from pathlib import Path
 from re import compile
diff -pruN 3.24.0-1/ase/io/siesta_input.py 3.26.0-1/ase/io/siesta_input.py
--- 3.24.0-1/ase/io/siesta_input.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/siesta_input.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """SiestaInput"""
 import warnings
 
diff -pruN 3.24.0-1/ase/io/siesta_output.py 3.26.0-1/ase/io/siesta_output.py
--- 3.24.0-1/ase/io/siesta_output.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/siesta_output.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.units import Bohr, Ry
diff -pruN 3.24.0-1/ase/io/sys.py 3.26.0-1/ase/io/sys.py
--- 3.24.0-1/ase/io/sys.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/sys.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 IO support for the qb@ll sys format.
 
diff -pruN 3.24.0-1/ase/io/trajectory.py 3.26.0-1/ase/io/trajectory.py
--- 3.24.0-1/ase/io/trajectory.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/trajectory.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Trajectory"""
 import contextlib
 import io
@@ -25,8 +27,8 @@ def Trajectory(filename, mode='r', atoms
 
     Parameters:
 
-    filename: str
-        The name of the file.  Traditionally ends in .traj.
+    filename: str | Path
+        The name/path of the file.  Traditionally ends in .traj.
     mode: str
         The mode.  'r' is read mode, the file should already exist, and
         no atoms argument should be specified.
@@ -66,7 +68,7 @@ class TrajectoryWriter:
 
         Parameters:
 
-        filename: str
+        filename: str | Path
             The name of the file.  Traditionally ends in .traj.
         mode: str
             The mode.  'r' is read mode, the file should already exist, and
diff -pruN 3.24.0-1/ase/io/turbomole.py 3.26.0-1/ase/io/turbomole.py
--- 3.24.0-1/ase/io/turbomole.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/turbomole.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.units import Bohr
 
 
diff -pruN 3.24.0-1/ase/io/ulm.py 3.26.0-1/ase/io/ulm.py
--- 3.24.0-1/ase/io/ulm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/ulm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 ULM files
 =========
diff -pruN 3.24.0-1/ase/io/utils.py 3.26.0-1/ase/io/utils.py
--- 3.24.0-1/ase/io/utils.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/utils.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,26 +1,197 @@
+# fmt: off
 from itertools import islice
-from math import sqrt
 from typing import IO
 
 import numpy as np
 
 from ase.data import atomic_numbers, covalent_radii
-from ase.data.colors import jmol_colors
+from ase.data.colors import jmol_colors as default_colors
 from ase.io.formats import string2index
-from ase.utils import rotate
+from ase.utils import irotate, rotate
+
+
+def normalize(a):
+    return np.array(a) / np.linalg.norm(a)
+
+
+def complete_camera_vectors(look=None, up=None, right=None):
+    """Creates the camera (or look) basis vectors from user input and
+     will autocomplete missing vector or non-orthogonal vectors using dot
+     products. The look direction will be maintained, up direction has higher
+     priority than right direction"""
+
+    # ensure good input
+    if look is not None:
+        assert len(look) == 3
+        l = np.array(look)
+
+    if up is not None:
+        assert len(up) == 3
+        u = np.array(up)
+
+    if right is not None:
+        assert len(right) == 3
+        r = np.array(right)
+
+    if look is not None and up is not None:
+        r = normalize(np.cross(l, u))
+        u = normalize(np.cross(r, l))  # ensures complete perpendicularity
+        l = normalize(np.cross(u, r))
+    elif look is not None and right is not None:
+        u = normalize(np.cross(r, l))
+        r = normalize(np.cross(l, u))  # ensures complete perpendicularity
+        l = normalize(np.cross(u, r))
+    elif up is not None and right is not None:
+        l = normalize(np.cross(u, r))
+        r = normalize(np.cross(l, u))  # ensures complete perpendicularity
+        u = normalize(np.cross(r, u))
+    else:
+        raise ValueError('''At least two camera vectors of <look>, <up>,
+ or <right> must be specified''')
+    return l, u, r
+
+
+def get_cell_vertex_points(cell, disp=(0.0, 0.0, 0.0)):
+    """Returns 8x3 list of the cell vertex coordinates"""
+    cell_vertices = np.empty((2, 2, 2, 3))
+    displacement = np.array(disp)
+    for c1 in range(2):
+        for c2 in range(2):
+            for c3 in range(2):
+                cell_vertices[c1, c2, c3] = [c1, c2, c3] @ cell + displacement
+    cell_vertices.shape = (8, 3)
+    return cell_vertices
+
+
+def update_line_order_for_atoms(L, T, D, atoms, radii):
+    # why/how does this happen before the camera rotation???
+    R = atoms.get_positions()
+    r2 = radii**2
+    for n in range(len(L)):
+        d = D[T[n]]
+        if ((((R - L[n] - d)**2).sum(1) < r2) &
+                (((R - L[n] + d)**2).sum(1) < r2)).any():
+            T[n] = -1
+    return T
+
+
+def combine_bboxes(bbox_a, bbox_b):
+    """Combines bboxes using their extrema"""
+    bbox_low = np.minimum(bbox_a[0], bbox_b[0])
+    bbox_high = np.maximum(bbox_a[1], bbox_b[1])
+    return np.array([bbox_low, bbox_high])
+
+
+def has_cell(atoms):
+    return atoms.cell.rank > 0
+
+
+HIDE = 0
+SHOW_CELL = 1
+SHOW_CELL_AND_FIT_TO_ALL = 2
+SHOW_CELL_AND_FIT_TO_CELL = 3
 
 
 class PlottingVariables:
     # removed writer - self
     def __init__(self, atoms, rotation='', show_unit_cell=2,
                  radii=None, bbox=None, colors=None, scale=20,
-                 maxwidth=500, extra_offset=(0., 0.)):
+                 maxwidth=500, extra_offset=(0., 0.),
+                 auto_bbox_size=1.05,
+                 auto_image_plane_z='front_all',
+                 ):
+
+        assert show_unit_cell in (0, 1, 2, 3)
+        """Handles camera/paper space transformations used for rendering, 2D
+        plots, ...and a few legacy features. after camera rotations, the image
+        plane is set to the front of structure.
+
+        atoms: Atoms object
+            The Atoms object to render/plot.
+
+        rotation: string or 3x3 matrix
+            Controls camera rotation. Can be a string with euler angles in
+            degrees like '45x, 90y, 0z' or a rotation matrix.
+            (defaults to '0x, 0y, 0z')
+
+        show_unit_cell: int 0, 1, 2, or 3
+            0 cell is not shown, 1 cell is shown, 2 cell is shown and bounding
+            box is computed to fit atoms and cell, 3 bounding box is fixed to
+            cell only. (default 2)
+
+        radii: list of floats
+            a list of atomic radii for the atoms. (default None)
+
+        bbox: list of four floats
+            Allows explicit control of the image plane bounding box in the form
+            (xlo, ylo, xhi, yhi) where x and y are the horizontal and vertical
+            axes of the image plane. The units are in atomic coordinates without
+            the paperspace scale factor. (defaults to None the automatic
+            bounding box is used)
+
+        colors : a list of RGB color triples
+            a list of the RGB color triples for each atom. (default None, uses
+            Jmol colors)
+
+        scale: float
+            The ratio between the image plane units and atomic units, e.g.
+            Angstroms per cm. (default 20.0)
+
+        maxwidth: float
+            Limits the width of the image plane. (why?) Uses paperspace units.
+            (default 500)
+
+        extra_offset: (float, float)
+            Translates the image center in the image plane by (x,y) where x and
+            y are the horizontal and vertical shift distances, respectively.
+            (default (0.0, 0.0)) should only be used for small tweaks to the
+            automatically fit image plane
+
+        auto_bbox_size: float
+            Controls the padding given to the bounding box in the image plane.
+            With auto_bbox_size=1.0 the structure touches the edges of the
+            image. auto_bbox_size>1.0 gives whitespace padding. (default 1.05)
+
+        auto_image_plane_z: string ('front_all', 'front_auto', 'legacy')
+            After a camera rotation, controls where to put camera image plane
+            relative to the atoms and cell. 'front_all' puts everything in front
+            of the camera. 'front_auto' sets the image plane location to
+            respect the show_unit_cell option so that the atoms or cell can be
+            ignored when setting the image plane. 'legacy' leaves the image
+            plane passing through the origin for backwards compatibility.
+            (default: 'front_all')
+        """
+
+        self.show_unit_cell = show_unit_cell
         self.numbers = atoms.get_atomic_numbers()
+        self.maxwidth = maxwidth
+        self.atoms = atoms
+        # not used in PlottingVariables, keeping for legacy
+        self.natoms = len(atoms)
+
+        self.auto_bbox_size = auto_bbox_size
+        self.auto_image_plane_z = auto_image_plane_z
+        self.offset = np.zeros(3)
+        self.extra_offset = np.array(extra_offset)
+
+        self.constraints = atoms.constraints
+        # extension for partial occupancies
+        self.frac_occ = False
+        self.tags = None
+        self.occs = None
+
+        if 'occupancy' in atoms.info:
+            self.occs = atoms.info['occupancy']
+            self.tags = atoms.get_tags()
+            self.frac_occ = True
+
+        # colors
         self.colors = colors
         if colors is None:
-            ncolors = len(jmol_colors)
-            self.colors = jmol_colors[self.numbers.clip(max=ncolors - 1)]
+            ncolors = len(default_colors)
+            self.colors = default_colors[self.numbers.clip(max=ncolors - 1)]
 
+        # radius
         if radii is None:
             radii = covalent_radii[self.numbers]
         elif isinstance(radii, float):
@@ -28,104 +199,251 @@ class PlottingVariables:
         else:
             radii = np.array(radii)
 
-        natoms = len(atoms)
+        self.radii = radii  # radius in Angstroms
+        self.scale = scale  # Angstroms per cm
 
-        if isinstance(rotation, str):
-            rotation = rotate(rotation)
+        self.set_rotation(rotation)
+        self.update_image_plane_offset_and_size_from_structure(bbox=bbox)
 
-        cell = atoms.get_cell()
-        disp = atoms.get_celldisp().flatten()
+    def to_dict(self):
+        out = {
+            'bbox': self.get_bbox(),
+            'rotation': self.rotation,
+            'scale':    self.scale,
+            'colors': self.colors}
+        return out
+
+    @property
+    def d(self):
+        # XXX hopefully this can be deprecated someday.
+        """Returns paperspace diameters for scale and radii lists"""
+        return 2 * self.scale * self.radii
+
+    def set_rotation(self, rotation):
+        if rotation is not None:
+            if isinstance(rotation, str):
+                rotation = rotate(rotation)
+            self.rotation = rotation
+        self.update_patch_and_line_vars()
+
+    def update_image_plane_offset_and_size_from_structure(self, bbox=None):
+        """Updates image size to fit structure according to show_unit_cell
+        if bbox=None. Otherwise, sets the image size from bbox. bbox is in the
+        image plane. Note that bbox format is (xlo, ylo, xhi, yhi) for
+        compatibility reasons the internal functions use (2,3)"""
+
+        # zero out the offset so it's not involved in the
+        # to_image_plane_positions() calculations which are used to calcucate
+        #  the offset
+        self.offset = np.zeros(3)
+
+        # computing the bboxes in self.atoms here makes it easier to follow the
+        # various options selection/choices later
+        bbox_atoms = self.get_bbox_from_atoms(self.atoms, self.d / 2)
+        if has_cell(self.atoms):
+            cell = self.atoms.get_cell()
+            disp = self.atoms.get_celldisp().flatten()
+            bbox_cell = self.get_bbox_from_cell(cell, disp)
+            bbox_combined = combine_bboxes(bbox_atoms, bbox_cell)
+        else:
+            bbox_combined = bbox_atoms
 
-        if show_unit_cell > 0:
-            L, T, D = cell_to_lines(self, cell)
-            cell_vertices = np.empty((2, 2, 2, 3))
-            for c1 in range(2):
-                for c2 in range(2):
-                    for c3 in range(2):
-                        cell_vertices[c1, c2, c3] = np.dot([c1, c2, c3],
-                                                           cell) + disp
-            cell_vertices.shape = (8, 3)
-            cell_vertices = np.dot(cell_vertices, rotation)
+        # bbox_auto is the bbox that matches the show_unit_cell option
+        if has_cell(self.atoms) and self.show_unit_cell in (
+            SHOW_CELL_AND_FIT_TO_ALL, SHOW_CELL_AND_FIT_TO_CELL):
+
+            if self.show_unit_cell == SHOW_CELL_AND_FIT_TO_ALL:
+                bbox_auto = bbox_combined
+            else:
+                bbox_auto = bbox_cell
         else:
-            L = np.empty((0, 3))
-            T = None
-            D = None
-            cell_vertices = None
+            bbox_auto = bbox_atoms
 
-        nlines = len(L)
+        #
+        if bbox is None:
+            middle = (bbox_auto[0] + bbox_auto[1]) / 2
+            im_size = self.auto_bbox_size * (bbox_auto[1] - bbox_auto[0])
+            # should auto_bbox_size pad the z_heght via offset?
+
+            if im_size[0] > self.maxwidth:
+                rescale_factor = self.maxwidth / im_size[0]
+                im_size *= rescale_factor
+                self.scale *= rescale_factor
+                middle *= rescale_factor  # center should be rescaled too
+            offset = middle - im_size / 2
+        else:
+            width = (bbox[2] - bbox[0]) * self.scale
+            height = (bbox[3] - bbox[1]) * self.scale
 
-        positions = np.empty((natoms + nlines, 3))
-        R = atoms.get_positions()
-        positions[:natoms] = R
-        positions[natoms:] = L
-
-        r2 = radii**2
-        for n in range(nlines):
-            d = D[T[n]]
-            if ((((R - L[n] - d)**2).sum(1) < r2) &
-                    (((R - L[n] + d)**2).sum(1) < r2)).any():
-                T[n] = -1
+            im_size = np.array([width, height, 0])
+            offset = np.array([bbox[0], bbox[1], 0]) * self.scale
 
-        positions = np.dot(positions, rotation)
-        R = positions[:natoms]
+        # this section shifts the image plane up and down parallel to the look
+        # direction to match the legacy option, or to force it allways touch the
+        # front most objects regardless of the show_unit_cell setting
+        if self.auto_image_plane_z == 'front_all':
+            offset[2] = bbox_combined[1, 2]  # highest z in image orientation
+        elif self.auto_image_plane_z == 'legacy':
+            offset[2] = 0
+        elif self.auto_image_plane_z == 'front_auto':
+            offset[2] = bbox_auto[1, 2]
+        else:
+            raise ValueError(
+                f'bad image plane setting {self.auto_image_plane_z!r}')
 
-        if bbox is None:
-            X1 = (R - radii[:, None]).min(0)
-            X2 = (R + radii[:, None]).max(0)
-            if show_unit_cell == 2:
-                X1 = np.minimum(X1, cell_vertices.min(0))
-                X2 = np.maximum(X2, cell_vertices.max(0))
-            M = (X1 + X2) / 2
-            S = 1.05 * (X2 - X1)
-            w = scale * S[0]
-            if w > maxwidth:
-                w = maxwidth
-                scale = w / S[0]
-            h = scale * S[1]
-            offset = np.array([scale * M[0] - w / 2, scale * M[1] - h / 2, 0])
-        else:
-            w = (bbox[2] - bbox[0]) * scale
-            h = (bbox[3] - bbox[1]) * scale
-            offset = np.array([bbox[0], bbox[1], 0]) * scale
-
-        offset[0] = offset[0] - extra_offset[0]
-        offset[1] = offset[1] - extra_offset[1]
-        self.w = w + extra_offset[0]
-        self.h = h + extra_offset[1]
-
-        positions *= scale
-        positions -= offset
-
-        if nlines > 0:
-            D = np.dot(D, rotation)[:, :2] * scale
-
-        if cell_vertices is not None:
-            cell_vertices *= scale
-            cell_vertices -= offset
+        # since we are moving the origin in the image plane (camera coordinates)
+        self.offset += offset
 
-        cell = np.dot(cell, rotation)
-        cell *= scale
+        # Previously, the picture size changed with extra_offset, This is very
+        # counter intuitive and seems like a bug. Leaving it commented out in
+        # case someone relying on this likely bug needs to revert it.
+        self.w = im_size[0]  # + self.extra_offset[0]
+        self.h = im_size[1]  # + self.extra_offset[1]
+
+        # allows extra_offset to be 2D or 3D
+        for i in range(len(self.extra_offset)):
+            self.offset[i] -= self.extra_offset[i]
+
+        # we have to update the arcane stuff after every camera update.
+        self.update_patch_and_line_vars()
+
+    def center_camera_on_position(self, pos, scaled_position=False):
+        if scaled_position:
+            pos = pos @ self.atoms.cell
+        im_pos = self.to_image_plane_positions(pos)
+        cam_pos = self.to_image_plane_positions(self.get_image_plane_center())
+        in_plane_shift = im_pos - cam_pos
+        self.offset[0:2] += in_plane_shift[0:2]
+        self.update_patch_and_line_vars()
+
+    def get_bbox(self):
+        xlo = self.offset[0]
+        ylo = self.offset[1]
+        xhi = xlo + self.w
+        yhi = ylo + self.h
+        return np.array([xlo, ylo, xhi, yhi]) / self.scale
+
+    def set_rotation_from_camera_directions(self,
+                                            look=None, up=None, right=None,
+                                            scaled_position=False):
+
+        if scaled_position:
+            if look is not None:
+                look = look @ self.atoms.cell
+            if right is not None:
+                right = right @ self.atoms.cell
+            if up is not None:
+                up = up @ self.atoms.cell
+
+        look, up, right = complete_camera_vectors(look, up, right)
+
+        rotation = np.zeros((3, 3))
+        rotation[:, 0] = right
+        rotation[:, 1] = up
+        rotation[:, 2] = -look
+        self.rotation = rotation
+        self.update_patch_and_line_vars()
+
+    def get_rotation_angles(self):
+        """Gets the rotation angles from the rotation matrix in the current
+        PlottingVariables object"""
+        return irotate(self.rotation)
+
+    def get_rotation_angles_string(self, digits=5):
+        fmt = '%.{:d}f'.format(digits)
+        angles = self.get_rotation_angles()
+        outstring = (fmt + 'x, ' + fmt + 'y, ' + fmt + 'z') % (angles)
+        return outstring
+
+    def update_patch_and_line_vars(self):
+        """Updates all the line and path stuff that is still inobvious, this
+        function should be deprecated if nobody can understand why it's features
+        exist."""
+        cell = self.atoms.get_cell()
+        disp = self.atoms.get_celldisp().flatten()
+        positions = self.atoms.get_positions()
 
-        self.cell = cell
-        self.positions = positions
+        if self.show_unit_cell in (
+            SHOW_CELL, SHOW_CELL_AND_FIT_TO_ALL, SHOW_CELL_AND_FIT_TO_CELL):
+
+            L, T, D = cell_to_lines(self, cell)
+            cell_verts_in_atom_coords = get_cell_vertex_points(cell, disp)
+            cell_vertices = self.to_image_plane_positions(
+                cell_verts_in_atom_coords)
+            T = update_line_order_for_atoms(L, T, D, self.atoms, self.radii)
+            # D are a positions in the image plane,
+            # not sure why it's setup like this
+            D = (self.to_image_plane_positions(D) + self.offset)[:, :2]
+            positions = np.concatenate((positions, L), axis=0)
+        else:
+            L = np.empty((0, 3))
+            T = None
+            D = None
+            cell_vertices = None
+        # just a rotations and scaling since offset is currently [0,0,0]
+        image_plane_positions = self.to_image_plane_positions(positions)
+        self.positions = image_plane_positions
+        # list of 2D cell points in the imageplane without the offset
         self.D = D
+        # integers, probably z-order for lines?
         self.T = T
         self.cell_vertices = cell_vertices
-        self.natoms = natoms
-        self.d = 2 * scale * radii
-        self.constraints = atoms.constraints
 
-        # extension for partial occupancies
-        self.frac_occ = False
-        self.tags = None
-        self.occs = None
-
-        try:
-            self.occs = atoms.info['occupancy']
-            self.tags = atoms.get_tags()
-            self.frac_occ = True
-        except KeyError:
-            pass
+        # no displacement since it's a vector
+        cell_vec_im = self.scale * self.atoms.get_cell() @ self.rotation
+        self.cell = cell_vec_im
+
+    def to_image_plane_positions(self, positions):
+        """Converts atomic coordinates to image plane positions. The third
+        coordinate is distance above/below the image plane"""
+        im_positions = (positions @ self.rotation) * self.scale - self.offset
+        return im_positions
+
+    def to_atom_positions(self, im_positions):
+        """Converts image plane positions to atomic coordinates."""
+        positions = ((im_positions + self.offset) /
+                     self.scale) @ self.rotation.T
+        return positions
+
+    def get_bbox_from_atoms(self, atoms, im_radii):
+        """Uses supplied atoms and radii to compute the bounding box of the
+        atoms in the image plane"""
+        im_positions = self.to_image_plane_positions(atoms.get_positions())
+        im_low = (im_positions - im_radii[:, None]).min(0)
+        im_high = (im_positions + im_radii[:, None]).max(0)
+        return np.array([im_low, im_high])
+
+    def get_bbox_from_cell(self, cell, disp=(0.0, 0.0, 0.0)):
+        """Uses supplied cell to compute the bounding box of the cell in the
+        image plane"""
+        displacement = np.array(disp)
+        cell_verts_in_atom_coords = get_cell_vertex_points(cell, displacement)
+        cell_vertices = self.to_image_plane_positions(cell_verts_in_atom_coords)
+        im_low = cell_vertices.min(0)
+        im_high = cell_vertices.max(0)
+        return np.array([im_low, im_high])
+
+    def get_image_plane_center(self):
+        return self.to_atom_positions(np.array([self.w / 2, self.h / 2, 0]))
+
+    def get_atom_direction(self, direction):
+        c0 = self.to_atom_positions([0, 0, 0])  # self.get_image_plane_center()
+        c1 = self.to_atom_positions(direction)
+        atom_direction = c1 - c0
+        return atom_direction / np.linalg.norm(atom_direction)
+
+    def get_camera_direction(self):
+        """Returns vector pointing away from camera toward atoms/cell in atomic
+        coordinates"""
+        return self.get_atom_direction([0, 0, -1])
+
+    def get_camera_up(self):
+        """Returns the image plane up direction in atomic coordinates"""
+        return self.get_atom_direction([0, 1, 0])
+
+    def get_camera_right(self):
+        """Returns the image plane right direction in atomic coordinates"""
+        return self.get_atom_direction([1, 0, 0])
 
 
 def cell_to_lines(writer, cell):
@@ -134,7 +452,7 @@ def cell_to_lines(writer, cell):
     nlines = 0
     nsegments = []
     for c in range(3):
-        d = sqrt((cell[c]**2).sum())
+        d = np.sqrt((cell[c]**2).sum())
         n = max(2, int(d / 0.3))
         nsegments.append(n)
         nlines += 4 * n
@@ -192,12 +510,14 @@ def make_patch_list(writer):
                         extent = 360. * occ
                         patch = Wedge(
                             xy, r, start, start + extent,
-                            facecolor=jmol_colors[atomic_numbers[sym]],
+                            facecolor=default_colors[atomic_numbers[sym]],
                             edgecolor='black')
                         patch_list.append(patch)
                         start += extent
 
             else:
+                # why are there more positions than atoms?
+                # is this related to the cell?
                 if ((xy[1] + r > 0) and (xy[1] - r < writer.h) and
                         (xy[0] + r > 0) and (xy[0] - r < writer.w)):
                     patch = Circle(xy, r, facecolor=writer.colors[a],
diff -pruN 3.24.0-1/ase/io/v_sim.py 3.26.0-1/ase/io/v_sim.py
--- 3.24.0-1/ase/io/v_sim.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/v_sim.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 This module contains functionality for reading and writing an ASE
 Atoms object in V_Sim 3.5+ ascii format.
diff -pruN 3.24.0-1/ase/io/vasp.py 3.26.0-1/ase/io/vasp.py
--- 3.24.0-1/ase/io/vasp.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/vasp.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,8 +1,12 @@
+# fmt: off
+
 """
 This module contains functionality for reading and writing an ASE
 Atoms object in VASP POSCAR format.
 
 """
+from __future__ import annotations
+
 import re
 from pathlib import Path
 from typing import List, Optional, TextIO, Tuple
@@ -15,6 +19,7 @@ from ase.io import ParseError
 from ase.io.formats import string2index
 from ase.io.utils import ImageIterator
 from ase.symbols import Symbols
+from ase.units import Ang, fs
 from ase.utils import reader, writer
 
 from .vasp_parsers import vasp_outcar_parsers as vop
@@ -140,17 +145,27 @@ def get_atomtypes_from_formula(formula):
 
 
 @reader
-def read_vasp(filename='CONTCAR'):
+def read_vasp(fd):
     """Import POSCAR/CONTCAR type file.
 
     Reads unitcell, atom positions and constraints from the POSCAR/CONTCAR
     file and tries to read atom types from POSCAR/CONTCAR header, if this
     fails the atom types are read from OUTCAR or POTCAR file.
     """
+    atoms = read_vasp_configuration(fd)
+    velocity_init_line = fd.readline()
+    if velocity_init_line.strip() and velocity_init_line[0].lower() == 'l':
+        read_lattice_velocities(fd)
+    velocities = read_velocities_if_present(fd, len(atoms))
+    if velocities is not None:
+        atoms.set_velocities(velocities)
+    return atoms
 
+
+def read_vasp_configuration(fd):
+    """Read common POSCAR/CONTCAR/CHGCAR/CHG quantities and return Atoms."""
     from ase.data import chemical_symbols
 
-    fd = filename
     # The first line is in principle a comment line, however in VASP
     # 4.x a common convention is to have it contain the atom symbols,
     # eg. "Ag Ge" in the same order as later in the file (and POTCAR
@@ -249,6 +264,7 @@ def read_vasp(filename='CONTCAR'):
         atoms_pos[atom] = [float(_) for _ in ac[0:3]]
         if selective_dynamics:
             selective_flags[atom] = [_ == 'F' for _ in ac[3:6]]
+
     atoms = Atoms(symbols=atom_symbols, cell=cell, pbc=True)
     if cartesian:
         atoms_pos *= scale
@@ -257,9 +273,41 @@ def read_vasp(filename='CONTCAR'):
         atoms.set_scaled_positions(atoms_pos)
     if selective_dynamics:
         set_constraints(atoms, selective_flags)
+
     return atoms
 
 
+def read_lattice_velocities(fd):
+    """
+    Read lattice velocities and vectors from POSCAR/CONTCAR.
+    As lattice velocities are not yet implemented in ASE, this function just
+    throws away these lines.
+    """
+    fd.readline()  # initialization state
+    for _ in range(3):  # lattice velocities
+        fd.readline()
+    for _ in range(3):  # lattice vectors
+        fd.readline()
+    fd.readline()  # get rid of 1 empty line below if it exists
+
+
+def read_velocities_if_present(fd, natoms) -> np.ndarray | None:
+    """Read velocities from POSCAR/CONTCAR if present, return in ASE units."""
+    # Check if it is the velocities block or the MD extra block
+    words = fd.readline().split()
+    if len(words) <= 1:  # MD extra block or end of file
+        return None
+    atoms_vel = np.empty((natoms, 3))
+    atoms_vel[0] = (float(words[0]), float(words[1]), float(words[2]))
+    for atom in range(1, natoms):
+        words = fd.readline().split()
+        assert len(words) == 3
+        atoms_vel[atom] = (float(words[0]), float(words[1]), float(words[2]))
+
+    # unit conversion from Angstrom/fs to ASE units
+    return atoms_vel * (Ang / fs)
+
+
 def set_constraints(atoms: Atoms, selective_flags: np.ndarray):
     """Set constraints based on selective_flags"""
     from ase.constraints import FixAtoms, FixConstraint, FixScaled
@@ -428,7 +476,7 @@ def read_vasp_xml(filename='vasprun.xml'
                         kpts_params = OrderedDict()
                         parameters['kpoints_generation'] = kpts_params
                         for par in subelem.iter():
-                            if par.tag in ['v', 'i']:
+                            if par.tag in ['v', 'i'] and "name" in par.attrib:
                                 parname = par.attrib['name'].lower()
                                 kpts_params[parname] = __get_xml_parameter(par)
 
@@ -846,6 +894,15 @@ def write_vasp(
             fd.write(''.join([f'{f:>4s}' for f in flags]))
         fd.write('\n')
 
+    # if velocities in atoms object write velocities
+    if atoms.has('momenta'):
+        cform = 3 * ' {:19.16f}' + '\n'
+        fd.write('Cartesian\n')
+        # unit conversion to Angstrom / fs
+        vel = atoms.get_velocities() / (Ang / fs)
+        for vatom in vel:
+            fd.write(cform.format(*vatom))
+
 
 def _handle_ase_constraints(atoms: Atoms) -> np.ndarray:
     """Convert the ASE constraints on `atoms` to VASP constraints
@@ -875,7 +932,7 @@ def _handle_ase_constraints(atoms: Atoms
             mask = np.all(
                 np.abs(np.cross(constr.dir, atoms.cell)) < 1e-5, axis=1
             )
-            if sum(mask) != 1:
+            if mask.sum() != 1:
                 raise RuntimeError(
                     'VASP requires that the direction of FixedPlane '
                     'constraints is parallel with one of the cell axis'
@@ -886,7 +943,7 @@ def _handle_ase_constraints(atoms: Atoms
             mask = np.all(
                 np.abs(np.cross(constr.dir, atoms.cell)) < 1e-5, axis=1
             )
-            if sum(mask) != 1:
+            if mask.sum() != 1:
                 raise RuntimeError(
                     'VASP requires that the direction of FixedLine '
                     'constraints is parallel with one of the cell axis'
diff -pruN 3.24.0-1/ase/io/vasp_parsers/__init__.py 3.26.0-1/ase/io/vasp_parsers/__init__.py
--- 3.24.0-1/ase/io/vasp_parsers/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/vasp_parsers/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from . import vasp_outcar_parsers
 
 __all__ = ('vasp_outcar_parsers', )
diff -pruN 3.24.0-1/ase/io/vasp_parsers/incar_writer.py 3.26.0-1/ase/io/vasp_parsers/incar_writer.py
--- 3.24.0-1/ase/io/vasp_parsers/incar_writer.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/vasp_parsers/incar_writer.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from collections.abc import Iterable
 
 
diff -pruN 3.24.0-1/ase/io/vasp_parsers/vasp_outcar_parsers.py 3.26.0-1/ase/io/vasp_parsers/vasp_outcar_parsers.py
--- 3.24.0-1/ase/io/vasp_parsers/vasp_outcar_parsers.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/vasp_parsers/vasp_outcar_parsers.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Module for parsing OUTCAR files.
 """
diff -pruN 3.24.0-1/ase/io/vtkxml.py 3.26.0-1/ase/io/vtkxml.py
--- 3.24.0-1/ase/io/vtkxml.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/vtkxml.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 fast = False
diff -pruN 3.24.0-1/ase/io/wannier90.py 3.26.0-1/ase/io/wannier90.py
--- 3.24.0-1/ase/io/wannier90.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/wannier90.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Read Wannier90 wout format."""
 from typing import IO, Any, Dict
 
diff -pruN 3.24.0-1/ase/io/wien2k.py 3.26.0-1/ase/io/wien2k.py
--- 3.24.0-1/ase/io/wien2k.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/wien2k.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/io/x3d.py 3.26.0-1/ase/io/x3d.py
--- 3.24.0-1/ase/io/x3d.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/x3d.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Output support for X3D and X3DOM file types.
 See http://www.web3d.org/x3d/specifications/
diff -pruN 3.24.0-1/ase/io/xsd.py 3.26.0-1/ase/io/xsd.py
--- 3.24.0-1/ase/io/xsd.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/xsd.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import xml.etree.ElementTree as ET
 from xml.dom import minidom
 
diff -pruN 3.24.0-1/ase/io/xsf.py 3.26.0-1/ase/io/xsf.py
--- 3.24.0-1/ase/io/xsf.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/xsf.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.atoms import Atoms
diff -pruN 3.24.0-1/ase/io/xtd.py 3.26.0-1/ase/io/xtd.py
--- 3.24.0-1/ase/io/xtd.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/xtd.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import xml.etree.ElementTree as ET
 from xml.dom import minidom
 
diff -pruN 3.24.0-1/ase/io/xyz.py 3.26.0-1/ase/io/xyz.py
--- 3.24.0-1/ase/io/xyz.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/xyz.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Reference implementation of reader and writer for standard XYZ files.
 
 See https://en.wikipedia.org/wiki/XYZ_file_format
diff -pruN 3.24.0-1/ase/io/zmatrix.py 3.26.0-1/ase/io/zmatrix.py
--- 3.24.0-1/ase/io/zmatrix.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/io/zmatrix.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import re
 from collections import namedtuple
 from numbers import Real
@@ -23,6 +25,12 @@ _ZMatrixRow = namedtuple(
 ThreeFloats = Union[Tuple[float, float, float], np.ndarray]
 
 
+def require(condition):
+    # (This is not good error handling, but it replaces assertions.)
+    if not condition:
+        raise RuntimeError('Internal requirement violated')
+
+
 class _ZMatrixToAtoms:
     known_units = dict(
         distance={'angstrom': Angstrom, 'bohr': Bohr, 'au': Bohr, 'nm': nm},
@@ -102,7 +110,8 @@ class _ZMatrixToAtoms:
 
         self.name_to_index[name] = self.nrows
 
-    def validate_indices(self, *indices: int) -> None:
+    # Use typehint *indices: str from python3.11+
+    def validate_indices(self, *indices) -> None:
         """Raises an error if indices in a Z-matrix row are invalid."""
         if any(np.array(indices) >= self.nrows):
             raise ValueError('An invalid Z-matrix was provided! Row {} refers '
@@ -123,19 +132,19 @@ class _ZMatrixToAtoms:
         name = tokens[0]
         self.set_index(name)
         if len(tokens) == 1:
-            assert self.nrows == 0
+            require(self.nrows == 0)
             return name, np.zeros(3, dtype=float)
 
         ind1 = self.get_index(tokens[1])
         if ind1 == -1:
-            assert len(tokens) == 5
+            require(len(tokens) == 5)
             return name, np.array(list(map(self.get_var, tokens[2:])),
                                   dtype=float)
 
         dist = self.dconv * self.get_var(tokens[2])
 
         if len(tokens) == 3:
-            assert self.nrows == 1
+            require(self.nrows == 1)
             self.validate_indices(ind1)
             return name, np.array([dist, 0, 0], dtype=float)
 
@@ -143,7 +152,7 @@ class _ZMatrixToAtoms:
         a_bend = self.aconv * self.get_var(tokens[4])
 
         if len(tokens) == 5:
-            assert self.nrows == 2
+            require(self.nrows == 2)
             self.validate_indices(ind1, ind2)
             return name, _ZMatrixRow(ind1, dist, ind2, a_bend, None, None)
 
diff -pruN 3.24.0-1/ase/lattice/__init__.py 3.26.0-1/ase/lattice/__init__.py
--- 3.24.0-1/ase/lattice/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/lattice/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,7 @@
+# fmt: off
+
 from abc import ABC, abstractmethod
+from functools import cached_property
 from typing import Dict, List
 
 import numpy as np
@@ -1130,7 +1133,59 @@ def get_lattice_from_canonical_cell(cell
 
     If the given cell does not resemble the known form of a Bravais
     lattice, raise RuntimeError."""
-    return LatticeChecker(cell, eps).match()
+    return NormalizedLatticeMatcher(cell, eps).match()
+
+
+class LatticeMatcher:
+    def __init__(self, cell, pbc, eps):
+        self.orig_cell = cell
+        self.pbc = cell.any(1) & pbc2pbc(pbc)
+        self.cell = cell.uncomplete(pbc)
+        self.eps = eps
+        self.niggli_cell, self.niggli_op = self.cell.niggli_reduce(eps=eps)
+
+        # We tabulate the cell's Niggli-mapped versions so we don't need to
+        # redo any work when the same Niggli-operation appears multiple times
+        # in the table:
+        self.memory = {}
+
+    def match(self, latname, operations):
+        matches = []
+
+        for op_key in operations:
+            checker_and_op = self.memory.get(op_key)
+            if checker_and_op is None:
+                # op_3x3 is the 3x3 form of the operation that maps
+                # Niggli cell to AFlow form.
+                op_3x3 = np.array(op_key).reshape(3, 3)
+                candidate = Cell(np.linalg.inv(op_3x3.T) @ self.niggli_cell)
+                checker = NormalizedLatticeMatcher(candidate, eps=self.eps)
+                self.memory[op_key] = (checker, op_3x3)
+            else:
+                checker, op_3x3 = checker_and_op
+
+            lat, err = checker.query(latname)
+            if lat is None or err > self.eps:
+                continue
+
+            # This is the full operation encompassing
+            # both Niggli reduction of user input and mapping the
+            # Niggli reduced form to standard (AFlow) form.
+            op = op_3x3 @ np.linalg.inv(self.niggli_op)
+            matches.append(Match(lat, op))
+
+        return matches
+
+
+class Match:
+    def __init__(self, lat, op):
+        self.lat = lat
+        self.op = op
+
+    @cached_property
+    def orthogonality_defect(self):
+        cell = self.lat.tocell().complete()
+        return np.prod(cell.lengths()) / cell.volume
 
 
 def identify_lattice(cell, eps=2e-4, *, pbc=True):
@@ -1141,50 +1196,26 @@ def identify_lattice(cell, eps=2e-4, *,
     and angles as the Bravais lattice object."""
     from ase.geometry.bravais_type_engine import niggli_op_table
 
-    pbc = cell.any(1) & pbc2pbc(pbc)
-    npbc = sum(pbc)
-
-    cell = cell.uncomplete(pbc)
-    rcell, reduction_op = cell.niggli_reduce(eps=eps)
-
-    # We tabulate the cell's Niggli-mapped versions so we don't need to
-    # redo any work when the same Niggli-operation appears multiple times
-    # in the table:
-    memory = {}
+    matcher = LatticeMatcher(cell, pbc, eps=eps)
 
     # We loop through the most symmetric kinds (CUB etc.) and return
     # the first one we find:
-    for latname in LatticeChecker.check_orders[npbc]:
+    for latname in lattice_check_orders[matcher.cell.rank]:
         # There may be multiple Niggli operations that produce valid
         # lattices, at least for MCL.  In that case we will pick the
         # one whose angle is closest to 90, but it means we cannot
-        # just return the first one we find so we must remember then:
-        matching_lattices = []
+        # just return the first one we find so we must remember them:
+        matches = matcher.match(latname, niggli_op_table[latname])
 
-        for op_key in niggli_op_table[latname]:
-            checker_and_op = memory.get(op_key)
-            if checker_and_op is None:
-                normalization_op = np.array(op_key).reshape(3, 3)
-                candidate = Cell(np.linalg.inv(normalization_op.T) @ rcell)
-                checker = LatticeChecker(candidate, eps=eps)
-                memory[op_key] = (checker, normalization_op)
-            else:
-                checker, normalization_op = checker_and_op
-
-            lat = checker.query(latname)
-            if lat is not None:
-                op = normalization_op @ np.linalg.inv(reduction_op)
-                matching_lattices.append((lat, op))
-
-        if not matching_lattices:
+        if not matches:
             continue  # Move to next Bravais lattice
 
-        lat, op = pick_best_lattice(matching_lattices)
+        best = min(matches, key=lambda match: match.orthogonality_defect)
 
-        if npbc == 2 and op[2, 2] < 0:
-            op = flip_2d_handedness(op)
+        if matcher.cell.rank == 2 and best.op[2, 2] < 0:
+            best.op = flip_2d_handedness(best.op)
 
-        return lat, op
+        return best.lat, best.op
 
     raise RuntimeError('Failed to recognize lattice')
 
@@ -1200,27 +1231,22 @@ def flip_2d_handedness(op):
     return repair_op @ op
 
 
-def pick_best_lattice(matching_lattices):
-    """Return (lat, op) with lowest orthogonality defect."""
-    best = None
-    best_defect = np.inf
-    for lat, op in matching_lattices:
-        cell = lat.tocell().complete()
-        orthogonality_defect = np.prod(cell.lengths()) / cell.volume
-        if orthogonality_defect < best_defect:
-            best = lat, op
-            best_defect = orthogonality_defect
-    return best
-
-
-class LatticeChecker:
-    # The check order is slightly different than elsewhere listed order
-    # as we need to check HEX/RHL before the ORCx family.
-    check_orders = {
-        1: ['LINE'],
-        2: ['SQR', 'RECT', 'HEX2D', 'CRECT', 'OBL'],
-        3: ['CUB', 'FCC', 'BCC', 'TET', 'BCT', 'HEX', 'RHL',
-            'ORC', 'ORCF', 'ORCI', 'ORCC', 'MCL', 'MCLC', 'TRI']}
+# Map of number of dimensions to order in which to check lattices.
+# We check most symmetric lattices first, so we can terminate early
+# when a match is found.
+#
+# The check order is slightly different than elsewhere listed order,
+# as we need to check HEX/RHL before the ORCx family.
+lattice_check_orders = {
+    1: ['LINE'],
+    2: ['SQR', 'RECT', 'HEX2D', 'CRECT', 'OBL'],
+    3: ['CUB', 'FCC', 'BCC', 'TET', 'BCT', 'HEX', 'RHL',
+        'ORC', 'ORCF', 'ORCI', 'ORCC', 'MCL', 'MCLC', 'TRI']}
+
+
+class NormalizedLatticeMatcher:
+    # This class checks that a candidate cell matches the normalized
+    # form of a Bravais lattice.  It's used internally by the LatticeMatcher.
 
     def __init__(self, cell, eps=2e-4):
         """Generate Bravais lattices that look (or not) like the given cell.
@@ -1251,24 +1277,21 @@ class LatticeChecker:
     def _check(self, latcls, *args):
         if any(arg <= 0 for arg in args):
             return None
+
         try:
-            lat = latcls(*args)
+            return latcls(*args)
         except UnconventionalLattice:
             return None
 
-        newcell = lat.tocell()
-        err = celldiff(self.cell, newcell)
-        if err < self.eps:
-            return lat
-
     def match(self):
         """Match cell against all lattices, returning most symmetric match.
 
         Returns the lattice object.  Raises RuntimeError on failure."""
-        for name in self.check_orders[self.cell.rank]:
-            lat = self.query(name)
-            if lat:
+        for name in lattice_check_orders[self.cell.rank]:
+            lat, err = self.query(name)
+            if lat and err < self.eps:
                 return lat
+
         raise RuntimeError('Could not find lattice type for cell '
                            'with lengths and angles {}'
                            .format(self.cell.cellpar().tolist()))
@@ -1278,8 +1301,14 @@ class LatticeChecker:
 
         Return lattice object on success, None on failure."""
         meth = getattr(self, latname)
+
         lat = meth()
-        return lat
+        if lat is None:
+            return None, None
+
+        newcell = lat.tocell()
+        err = celldiff(self.cell, newcell)
+        return lat, err
 
     def LINE(self):
         return self._check(LINE, self.lengths[0])
diff -pruN 3.24.0-1/ase/lattice/bravais.py 3.26.0-1/ase/lattice/bravais.py
--- 3.24.0-1/ase/lattice/bravais.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/lattice/bravais.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Bravais.py - class for generating Bravais lattices etc.
 
 This is a base class for numerous classes setting up pieces of crystal.
diff -pruN 3.24.0-1/ase/lattice/compounds.py 3.26.0-1/ase/lattice/compounds.py
--- 3.24.0-1/ase/lattice/compounds.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/lattice/compounds.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Function-like objects creating lattices with more than one element.
 
 These lattice creators are mainly intended as examples for how to build you
diff -pruN 3.24.0-1/ase/lattice/cubic.py 3.26.0-1/ase/lattice/cubic.py
--- 3.24.0-1/ase/lattice/cubic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/lattice/cubic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Function-like objects creating cubic lattices (SC, FCC, BCC and Diamond).
 
 The following lattice creators are defined:
diff -pruN 3.24.0-1/ase/lattice/hexagonal.py 3.26.0-1/ase/lattice/hexagonal.py
--- 3.24.0-1/ase/lattice/hexagonal.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/lattice/hexagonal.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Function-like object creating hexagonal lattices.
 
 The following lattice creators are defined:
diff -pruN 3.24.0-1/ase/lattice/monoclinic.py 3.26.0-1/ase/lattice/monoclinic.py
--- 3.24.0-1/ase/lattice/monoclinic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/lattice/monoclinic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Function-like object creating monoclinic lattices.
 
 The following lattice creator is defined:
diff -pruN 3.24.0-1/ase/lattice/orthorhombic.py 3.26.0-1/ase/lattice/orthorhombic.py
--- 3.24.0-1/ase/lattice/orthorhombic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/lattice/orthorhombic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Function-like objects creating orthorhombic lattices.
 
 The following lattice creators are defined:
diff -pruN 3.24.0-1/ase/lattice/tetragonal.py 3.26.0-1/ase/lattice/tetragonal.py
--- 3.24.0-1/ase/lattice/tetragonal.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/lattice/tetragonal.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Function-like objects creating tetragonal lattices.
 
 The following lattice creators are defined:
diff -pruN 3.24.0-1/ase/lattice/triclinic.py 3.26.0-1/ase/lattice/triclinic.py
--- 3.24.0-1/ase/lattice/triclinic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/lattice/triclinic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Function-like object creating triclinic lattices.
 
 The following lattice creator is defined:
diff -pruN 3.24.0-1/ase/md/analysis.py 3.26.0-1/ase/md/analysis.py
--- 3.24.0-1/ase/md/analysis.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/analysis.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 import numpy as np
 
diff -pruN 3.24.0-1/ase/md/andersen.py 3.26.0-1/ase/md/andersen.py
--- 3.24.0-1/ase/md/andersen.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/andersen.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,11 +1,11 @@
 """Andersen dynamics class."""
-from typing import IO, Optional, Union
+
+import warnings
 
 from numpy import cos, log, ones, pi, random, repeat
 
 from ase import Atoms, units
 from ase.md.md import MolecularDynamics
-from ase.parallel import DummyMPI, world
 
 
 class Andersen(MolecularDynamics):
@@ -18,16 +18,12 @@ class Andersen(MolecularDynamics):
         temperature_K: float,
         andersen_prob: float,
         fixcm: bool = True,
-        trajectory: Optional[str] = None,
-        logfile: Optional[Union[IO, str]] = None,
-        loginterval: int = 1,
-        communicator=world,
         rng=random,
-        append_trajectory: bool = False,
+        **kwargs,
     ):
-        """"
-        Parameters:
-
+        """
+        Parameters
+        ----------
         atoms: Atoms object
             The list of atoms.
 
@@ -49,24 +45,9 @@ class Andersen(MolecularDynamics):
             Random number generator. This must have the ``random`` method
             with the same signature as ``numpy.random.random``.
 
-        logfile: file object or str (optional)
-            If *logfile* is a string, a file with that name will be opened.
-            Use '-' for stdout.
-
-        trajectory: Trajectory object or str (optional)
-            Attach trajectory object. If *trajectory* is a string a
-            Trajectory will be constructed. Use *None* (the default) for no
-            trajectory.
-
-        communicator: MPI communicator (optional)
-            Communicator used to distribute random numbers to all tasks.
-            Default: ase.parallel.world. Set to None to disable communication.
-
-        append_trajectory: bool (optional)
-            Defaults to False, which causes the trajectory file to be
-            overwritten each time the dynamics is restarted from scratch.
-            If True, the new structures are appended to the trajectory
-            file instead.
+        **kwargs : dict, optional
+            Additional arguments passed to :class:~ase.md.md.MolecularDynamics
+            base class.
 
         The temperature is imposed by stochastic collisions with a heat bath
         that acts on velocity components of randomly chosen particles.
@@ -75,16 +56,18 @@ class Andersen(MolecularDynamics):
 
         H. C. Andersen, J. Chem. Phys. 72 (4), 2384–2393 (1980)
         """
+        if 'communicator' in kwargs:
+            msg = (
+                '`communicator` has been deprecated since ASE 3.25.0 '
+                'and will be removed in ASE 3.26.0. Use `comm` instead.'
+            )
+            warnings.warn(msg, FutureWarning)
+            kwargs['comm'] = kwargs.pop('communicator')
         self.temp = units.kB * temperature_K
         self.andersen_prob = andersen_prob
         self.fix_com = fixcm
         self.rng = rng
-        if communicator is None:
-            communicator = DummyMPI()
-        self.communicator = communicator
-        MolecularDynamics.__init__(self, atoms, timestep, trajectory,
-                                   logfile, loginterval,
-                                   append_trajectory=append_trajectory)
+        MolecularDynamics.__init__(self, atoms, timestep, **kwargs)
 
     def set_temperature(self, temperature_K):
         self.temp = units.kB * temperature_K
@@ -98,13 +81,13 @@ class Andersen(MolecularDynamics):
     def boltzmann_random(self, width, size):
         x = self.rng.random(size=size)
         y = self.rng.random(size=size)
-        z = width * cos(2 * pi * x) * (-2 * log(1 - y))**0.5
+        z = width * cos(2 * pi * x) * (-2 * log(1 - y)) ** 0.5
         return z
 
     def get_maxwell_boltzmann_velocities(self):
         natoms = len(self.atoms)
         masses = repeat(self.masses, 3).reshape(natoms, 3)
-        width = (self.temp / masses)**0.5
+        width = (self.temp / masses) ** 0.5
         velos = self.boltzmann_random(width, size=(natoms, 3))
         return velos  # [[x, y, z],] components for each atom
 
@@ -124,10 +107,11 @@ class Andersen(MolecularDynamics):
 
         if self.fix_com:
             # add random velocity to center of mass to prepare Andersen
-            width = (self.temp / sum(self.masses))**0.5
-            self.random_com_velocity = (ones(self.v.shape)
-                                        * self.boltzmann_random(width, (3)))
-            self.communicator.broadcast(self.random_com_velocity, 0)
+            width = (self.temp / sum(self.masses)) ** 0.5
+            self.random_com_velocity = ones(
+                self.v.shape
+            ) * self.boltzmann_random(width, (3))
+            self.comm.broadcast(self.random_com_velocity, 0)
             self.v += self.random_com_velocity
 
         self.v += 0.5 * forces / self.masses * self.dt
@@ -135,10 +119,11 @@ class Andersen(MolecularDynamics):
         # apply Andersen thermostat
         self.random_velocity = self.get_maxwell_boltzmann_velocities()
         self.andersen_chance = self.rng.random(size=self.v.shape)
-        self.communicator.broadcast(self.random_velocity, 0)
-        self.communicator.broadcast(self.andersen_chance, 0)
-        self.v[self.andersen_chance <= self.andersen_prob] \
-            = self.random_velocity[self.andersen_chance <= self.andersen_prob]
+        self.comm.broadcast(self.random_velocity, 0)
+        self.comm.broadcast(self.andersen_chance, 0)
+        self.v[self.andersen_chance <= self.andersen_prob] = (
+            self.random_velocity[self.andersen_chance <= self.andersen_prob]
+        )
 
         x = atoms.get_positions()
         if self.fix_com:
diff -pruN 3.24.0-1/ase/md/bussi.py 3.26.0-1/ase/md/bussi.py
--- 3.24.0-1/ase/md/bussi.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/bussi.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Bussi NVT dynamics class."""
 
 import math
@@ -10,23 +12,9 @@ from ase.md.verlet import VelocityVerlet
 
 class Bussi(VelocityVerlet):
     """Bussi stochastic velocity rescaling (NVT) molecular dynamics.
-    Based on the paper from Bussi et al. (https://arxiv.org/abs/0803.4060)
 
-    Parameters
-    ----------
-    atoms : Atoms
-        The atoms object.
-    timestep : float
-        The time step in ASE time units.
-    temperature_K : float
-        The desired temperature, in Kelvin.
-    taut : float
-        Time constant for Bussi temperature coupling in ASE time units.
-    rng : numpy.random, optional
-        Random number generator.
-    **md_kwargs : dict, optional
-        Additional arguments passed to :class:~ase.md.md.MolecularDynamics
-        base class.
+    Based on the paper from Bussi et al., J. Chem. Phys. 126, 014101 (2007)
+    (also available from https://arxiv.org/abs/0803.4060).
     """
 
     def __init__(
@@ -35,14 +23,34 @@ class Bussi(VelocityVerlet):
         timestep,
         temperature_K,
         taut,
-        rng=np.random,
-        **md_kwargs,
+        rng=None,
+        **kwargs,
     ):
-        super().__init__(atoms, timestep, **md_kwargs)
+        """
+        Parameters
+        ----------
+        atoms : Atoms
+            The atoms object.
+        timestep : float
+            The time step in ASE time units.
+        temperature_K : float
+            The desired temperature, in Kelvin.
+        taut : float
+            Time constant for Bussi temperature coupling in ASE time units.
+        rng : RNG object, optional
+            Random number generator, by default numpy.random.
+        **kwargs : dict, optional
+            Additional arguments are passed to
+            :class:~ase.md.md.MolecularDynamics base class.
+        """
+        super().__init__(atoms, timestep, **kwargs)
 
         self.temp = temperature_K * units.kB
         self.taut = taut
-        self.rng = rng
+        if rng is None:
+            self.rng = np.random
+        else:
+            self.rng = rng
 
         self.ndof = self.atoms.get_number_of_degrees_of_freedom()
 
@@ -83,7 +91,11 @@ class Bussi(VelocityVerlet):
         )
 
         # R1 in Eq. (A7)
-        normal_noise = self.rng.standard_normal()
+        noisearray = self.rng.standard_normal(size=(1,))
+        # ASE mpi interfaces can only broadcast arrays, not scalars
+        self.comm.broadcast(noisearray, 0)
+        normal_noise = noisearray[0]
+
         # \sum_{i=2}^{Nf} R_i^2 in Eq. (A7)
         # 2 * standard_gamma(n / 2) is equal to chisquare(n)
         sum_of_noises = 2.0 * self.rng.standard_gamma(0.5 * (self.ndof - 1))
diff -pruN 3.24.0-1/ase/md/contour_exploration.py 3.26.0-1/ase/md/contour_exploration.py
--- 3.24.0-1/ase/md/contour_exploration.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/contour_exploration.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import IO, Optional, Union
 
 import numpy as np
@@ -209,7 +211,7 @@ class ContourExploration(Dynamics):
         """ Call Dynamics.run and adjust max_steps """
         return Dynamics.run(self, steps=steps)
 
-    def log(self):
+    def log(self, gradient):
         if self.logfile is not None:
             # name = self.__class__.__name__
             if self.nsteps == 0:
@@ -238,7 +240,7 @@ class ContourExploration(Dynamics):
 
     def rand_vect(self):
         '''Returns a random (Natoms,3) vector'''
-        vect = self.rng.random((len(self._actual_atoms), 3)) - 0.5
+        vect = self.rng.normal(size=(len(self._actual_atoms), 3))
         return vect
 
     def create_drift_unit_vector(self, N, T):
diff -pruN 3.24.0-1/ase/md/fix.py 3.26.0-1/ase/md/fix.py
--- 3.24.0-1/ase/md/fix.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/fix.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 
diff -pruN 3.24.0-1/ase/md/langevin.py 3.26.0-1/ase/md/langevin.py
--- 3.24.0-1/ase/md/langevin.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/langevin.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,11 +1,12 @@
 """Langevin dynamics class."""
-from typing import IO, Optional, Union
+
+import warnings
+from typing import Optional
 
 import numpy as np
 
 from ase import Atoms, units
 from ase.md.md import MolecularDynamics
-from ase.parallel import DummyMPI, world
 
 
 class Langevin(MolecularDynamics):
@@ -23,16 +24,12 @@ class Langevin(MolecularDynamics):
         fixcm: bool = True,
         *,
         temperature_K: Optional[float] = None,
-        trajectory: Optional[str] = None,
-        logfile: Optional[Union[IO, str]] = None,
-        loginterval: int = 1,
-        communicator=world,
         rng=None,
-        append_trajectory: bool = False,
+        **kwargs,
     ):
         """
-        Parameters:
-
+        Parameters
+        ----------
         atoms: Atoms object
             The list of atoms.
 
@@ -59,24 +56,9 @@ class Langevin(MolecularDynamics):
             standard_normal method matching the signature of
             numpy.random.standard_normal.
 
-        logfile: file object or str (optional)
-            If *logfile* is a string, a file with that name will be opened.
-            Use '-' for stdout.
-
-        trajectory: Trajectory object or str (optional)
-            Attach trajectory object.  If *trajectory* is a string a
-            Trajectory will be constructed.  Use *None* (the default) for no
-            trajectory.
-
-        communicator: MPI communicator (optional)
-            Communicator used to distribute random numbers to all tasks.
-            Default: ase.parallel.world. Set to None to disable communication.
-
-        append_trajectory: bool (optional)
-            Defaults to False, which causes the trajectory file to be
-            overwritten each time the dynamics is restarted from scratch.
-            If True, the new structures are appended to the trajectory
-            file instead.
+        **kwargs : dict, optional
+            Additional arguments passed to :class:~ase.md.md.MolecularDynamics
+            base class.
 
         The temperature and friction are normally scalars, but in principle one
         quantity per atom could be specified by giving an array.
@@ -89,34 +71,44 @@ class Langevin(MolecularDynamics):
         propagator in Eq. 21/34; but that propagator is not quasi-symplectic
         and gives a systematic offset in the temperature at large time steps.
         """
+        if 'communicator' in kwargs:
+            msg = (
+                '`communicator` has been deprecated since ASE 3.25.0 '
+                'and will be removed in ASE 3.26.0. Use `comm` instead.'
+            )
+            warnings.warn(msg, FutureWarning)
+            kwargs['comm'] = kwargs.pop('communicator')
+
         if friction is None:
             raise TypeError("Missing 'friction' argument.")
         self.fr = friction
-        self.temp = units.kB * self._process_temperature(temperature,
-                                                         temperature_K, 'eV')
+        self.temp = units.kB * self._process_temperature(
+            temperature, temperature_K, 'eV'
+        )
         self.fix_com = fixcm
-        if communicator is None:
-            communicator = DummyMPI()
-        self.communicator = communicator
+
         if rng is None:
             self.rng = np.random
         else:
             self.rng = rng
-        MolecularDynamics.__init__(self, atoms, timestep, trajectory,
-                                   logfile, loginterval,
-                                   append_trajectory=append_trajectory)
+        MolecularDynamics.__init__(self, atoms, timestep, **kwargs)
         self.updatevars()
 
     def todict(self):
         d = MolecularDynamics.todict(self)
-        d.update({'temperature_K': self.temp / units.kB,
-                  'friction': self.fr,
-                  'fixcm': self.fix_com})
+        d.update(
+            {
+                'temperature_K': self.temp / units.kB,
+                'friction': self.fr,
+                'fixcm': self.fix_com,
+            }
+        )
         return d
 
     def set_temperature(self, temperature=None, temperature_K=None):
-        self.temp = units.kB * self._process_temperature(temperature,
-                                                         temperature_K, 'eV')
+        self.temp = units.kB * self._process_temperature(
+            temperature, temperature_K, 'eV'
+        )
         self.updatevars()
 
     def set_friction(self, friction):
@@ -134,11 +126,11 @@ class Langevin(MolecularDynamics):
         masses = self.masses
         sigma = np.sqrt(2 * T * fr / masses)
 
-        self.c1 = dt / 2. - dt * dt * fr / 8.
-        self.c2 = dt * fr / 2 - dt * dt * fr * fr / 8.
-        self.c3 = np.sqrt(dt) * sigma / 2. - dt**1.5 * fr * sigma / 8.
+        self.c1 = dt / 2.0 - dt * dt * fr / 8.0
+        self.c2 = dt * fr / 2 - dt * dt * fr * fr / 8.0
+        self.c3 = np.sqrt(dt) * sigma / 2.0 - dt**1.5 * fr * sigma / 8.0
         self.c5 = dt**1.5 * sigma / (2 * np.sqrt(3))
-        self.c4 = fr / 2. * self.c5
+        self.c4 = fr / 2.0 * self.c5
 
     def step(self, forces=None):
         atoms = self.atoms
@@ -164,22 +156,28 @@ class Langevin(MolecularDynamics):
                 constraint.redistribute_forces_md(atoms, xi, rand=True)
                 constraint.redistribute_forces_md(atoms, eta, rand=True)
 
-        self.communicator.broadcast(xi, 0)
-        self.communicator.broadcast(eta, 0)
+        self.comm.broadcast(xi, 0)
+        self.comm.broadcast(eta, 0)
 
         # To keep the center of mass stationary, we have to calculate
         # the random perturbations to the positions and the momenta,
-        # and make sure that they sum to zero.
+        # and make sure that they sum to zero.  This perturbs the
+        # temperature slightly, and we have to correct.
         self.rnd_pos = self.c5 * eta
         self.rnd_vel = self.c3 * xi - self.c4 * eta
         if self.fix_com:
+            factor = np.sqrt(natoms / (natoms - 1.0))
             self.rnd_pos -= self.rnd_pos.sum(axis=0) / natoms
-            self.rnd_vel -= (self.rnd_vel *
-                             self.masses).sum(axis=0) / (self.masses * natoms)
+            self.rnd_vel -= (self.rnd_vel * self.masses).sum(axis=0) / (
+                self.masses * natoms
+            )
+            self.rnd_pos *= factor
+            self.rnd_vel *= factor
 
         # First halfstep in the velocity.
-        self.v += (self.c1 * forces / self.masses - self.c2 * self.v +
-                   self.rnd_vel)
+        self.v += (
+            self.c1 * forces / self.masses - self.c2 * self.v + self.rnd_vel
+        )
 
         # Full step in positions
         x = atoms.get_positions()
@@ -192,8 +190,9 @@ class Langevin(MolecularDynamics):
         forces = atoms.get_forces(md=True)
 
         # Update the velocities
-        self.v += (self.c1 * forces / self.masses - self.c2 * self.v +
-                   self.rnd_vel)
+        self.v += (
+            self.c1 * forces / self.masses - self.c2 * self.v + self.rnd_vel
+        )
 
         # Second part of RATTLE taken care of here
         atoms.set_momenta(self.v * self.masses)
diff -pruN 3.24.0-1/ase/md/logger.py 3.26.0-1/ase/md/logger.py
--- 3.24.0-1/ase/md/logger.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/logger.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Logging for molecular dynamics."""
 import weakref
 from typing import IO, Any, Union
diff -pruN 3.24.0-1/ase/md/md.py 3.26.0-1/ase/md/md.py
--- 3.24.0-1/ase/md/md.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/md.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Molecular Dynamics."""
 import warnings
 from typing import IO, Optional, Union
@@ -171,8 +173,9 @@ class MolecularDynamics(Dynamics):
     def get_time(self):
         return self.nsteps * self.dt
 
-    def converged(self):
+    def converged(self, gradient=None):
         """ MD is 'converged' when number of maximum steps is reached. """
+        # We take gradient now (due to optimizers).  Should refactor.
         return self.nsteps >= self.max_steps
 
     def _get_com_velocity(self, velocity):
diff -pruN 3.24.0-1/ase/md/nose_hoover_chain.py 3.26.0-1/ase/md/nose_hoover_chain.py
--- 3.24.0-1/ase/md/nose_hoover_chain.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/nose_hoover_chain.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,6 +1,9 @@
+# fmt: off
+
 from __future__ import annotations
 
 import numpy as np
+from scipy.special import exprel
 
 import ase.units
 from ase import Atoms
@@ -65,14 +68,6 @@ class NoseHooverChainNVT(MolecularDynami
             The number of thermostat variables in the Nose-Hoover chain.
         tloop: int
             The number of sub-steps in thermostat integration.
-        trajectory: str or None
-            If `trajectory` is str, `Trajectory` will be instantiated.
-            Set `None` for no trajectory.
-        logfile: IO or str or None
-            If `logfile` is str, a file with that name will be opened.
-            Set `-` to output into stdout.
-        loginterval: int
-            Write a log line for every `loginterval` time steps.
         **kwargs : dict, optional
             Additional arguments passed to :class:~ase.md.md.MolecularDynamics
             base class.
@@ -84,7 +79,9 @@ class NoseHooverChainNVT(MolecularDynami
         )
         assert self.masses.shape == (len(self.atoms), 1)
 
+        num_atoms = self.atoms.get_global_number_of_atoms()
         self._thermostat = NoseHooverChainThermostat(
+            num_atoms_global=num_atoms,
             masses=self.masses,
             temperature_K=temperature_K,
             tdamp=tdamp,
@@ -143,6 +140,7 @@ class NoseHooverChainThermostat:
     """
     def __init__(
         self,
+        num_atoms_global: int,
         masses: np.ndarray,
         temperature_K: float,
         tdamp: float,
@@ -150,8 +148,8 @@ class NoseHooverChainThermostat:
         tloop: int = 1,
     ):
         """See `NoseHooverChainNVT` for the parameters."""
-        self._num_atoms = masses.shape[0]
-        self._masses = masses  # (num_atoms, 1)
+        self._num_atoms_global = num_atoms_global
+        self._masses = masses  # (len(atoms), 1)
         self._tdamp = tdamp
         self._tchain = tchain
         self._tloop = tloop
@@ -160,7 +158,7 @@ class NoseHooverChainThermostat:
 
         assert tchain >= 1
         self._Q = np.zeros(tchain)
-        self._Q[0] = 3 * self._num_atoms * self._kT * tdamp**2
+        self._Q[0] = 3 * self._num_atoms_global * self._kT * tdamp**2
         self._Q[1:] = self._kT * tdamp**2
 
         # The following variables are updated during self.step()
@@ -170,7 +168,7 @@ class NoseHooverChainThermostat:
     def get_thermostat_energy(self) -> float:
         """Return energy-like contribution from the thermostat variables."""
         energy = (
-            3 * self._num_atoms * self._kT * self._eta[0]
+            3 * self._num_atoms_global * self._kT * self._eta[0]
             + self._kT * np.sum(self._eta[1:])
             + np.sum(0.5 * self._p_eta**2 / self._Q)
         )
@@ -181,44 +179,596 @@ class NoseHooverChainThermostat:
         for _ in range(self._tloop):
             for coeff in FOURTH_ORDER_COEFFS:
                 p = self._integrate_nhc_loop(
-                    p, coeff * delta / len(FOURTH_ORDER_COEFFS)
+                    p, coeff * delta / self._tloop
                 )
 
         return p
 
+    def _integrate_p_eta_j(self, p: np.ndarray, j: int,
+                           delta2: float, delta4: float) -> None:
+        if j < self._tchain - 1:
+            self._p_eta[j] *= np.exp(
+                -delta4 * self._p_eta[j + 1] / self._Q[j + 1]
+            )
+
+        if j == 0:
+            g_j = np.sum(p**2 / self._masses) \
+                - 3 * self._num_atoms_global * self._kT
+        else:
+            g_j = self._p_eta[j - 1] ** 2 / self._Q[j - 1] - self._kT
+        self._p_eta[j] += delta2 * g_j
+
+        if j < self._tchain - 1:
+            self._p_eta[j] *= np.exp(
+                -delta4 * self._p_eta[j + 1] / self._Q[j + 1]
+            )
+
+    def _integrate_eta(self, delta: float) -> None:
+        self._eta += delta * self._p_eta / self._Q
+
+    def _integrate_nhc_p(self, p: np.ndarray, delta: float) -> None:
+        p *= np.exp(-delta * self._p_eta[0] / self._Q[0])
+
     def _integrate_nhc_loop(self, p: np.ndarray, delta: float) -> np.ndarray:
         delta2 = delta / 2
         delta4 = delta / 4
 
-        def _integrate_p_eta_j(p: np.ndarray, j: int) -> None:
-            if j < self._tchain - 1:
-                self._p_eta[j] *= np.exp(
-                    -delta4 * self._p_eta[j + 1] / self._Q[j + 1]
-                )
+        for j in range(self._tchain):
+            self._integrate_p_eta_j(p, self._tchain - j - 1, delta2, delta4)
+        self._integrate_eta(delta)
+        self._integrate_nhc_p(p, delta)
+        for j in range(self._tchain):
+            self._integrate_p_eta_j(p, j, delta2, delta4)
+
+        return p
+
+
+class IsotropicMTKNPT(MolecularDynamics):
+    """Isothermal-isobaric molecular dynamics with isotropic volume fluctuations
+    by Martyna-Tobias-Klein (MTK) method [1].
+
+    See also `NoseHooverChainNVT` for the references.
+
+    - [1] G. J. Martyna, D. J. Tobias, and M. L. Klein, J. Chem. Phys. 101,
+          4177-4189 (1994). https://doi.org/10.1063/1.467468
+    """
+    def __init__(
+        self,
+        atoms: Atoms,
+        timestep: float,
+        temperature_K: float,
+        pressure_au: float,
+        tdamp: float,
+        pdamp: float,
+        tchain: int = 3,
+        pchain: int = 3,
+        tloop: int = 1,
+        ploop: int = 1,
+        **kwargs,
+    ):
+        """
+        Parameters
+        ----------
+        atoms: ase.Atoms
+            The atoms object.
+        timestep: float
+            The time step in ASE time units.
+        temperature_K: float
+            The target temperature in K.
+        pressure_au: float
+            The external pressure in eV/Ang^3.
+        tdamp: float
+            The characteristic time scale for the thermostat in ASE time units.
+            Typically, it is set to 100 times of `timestep`.
+        pdamp: float
+            The characteristic time scale for the barostat in ASE time units.
+            Typically, it is set to 1000 times of `timestep`.
+        tchain: int
+            The number of thermostat variables in the Nose-Hoover thermostat.
+        pchain: int
+            The number of barostat variables in the MTK barostat.
+        tloop: int
+            The number of sub-steps in thermostat integration.
+        ploop: int
+            The number of sub-steps in barostat integration.
+        **kwargs : dict, optional
+            Additional arguments passed to :class:~ase.md.md.MolecularDynamics
+            base class.
+        """
+        super().__init__(
+            atoms=atoms,
+            timestep=timestep,
+            **kwargs,
+        )
+        assert self.masses.shape == (len(self.atoms), 1)
+
+        if len(atoms.constraints) > 0:
+            raise NotImplementedError(
+                "Current implementation does not support constraints"
+            )
+
+        self._num_atoms_global = self.atoms.get_global_number_of_atoms()
+        self._thermostat = NoseHooverChainThermostat(
+            num_atoms_global=self._num_atoms_global,
+            masses=self.masses,
+            temperature_K=temperature_K,
+            tdamp=tdamp,
+            tchain=tchain,
+            tloop=tloop,
+        )
+        self._barostat = IsotropicMTKBarostat(
+            num_atoms_global=self._num_atoms_global,
+            temperature_K=temperature_K,
+            pdamp=pdamp,
+            pchain=pchain,
+            ploop=ploop,
+        )
+
+        self._temperature_K = temperature_K
+        self._pressure_au = pressure_au
+
+        self._kT = ase.units.kB * self._temperature_K
+        self._volume0 = self.atoms.get_volume()
+        self._cell0 = np.array(self.atoms.get_cell())
+
+        # The following variables are updated during self.step()
+        self._q = self.atoms.get_positions()  # positions
+        self._p = self.atoms.get_momenta()  # momenta
+        self._eps = 0.0  # volume
+        self._p_eps = 0.0  # volume momenta
+
+    def step(self) -> None:
+        dt2 = self.dt / 2
+
+        self._p_eps = self._barostat.integrate_nhc_baro(self._p_eps, dt2)
+        self._p = self._thermostat.integrate_nhc(self._p, dt2)
+        self._integrate_p_cell(dt2)
+        self._integrate_p(dt2)
+        self._integrate_q(self.dt)
+        self._integrate_q_cell(self.dt)
+        self._integrate_p(dt2)
+        self._integrate_p_cell(dt2)
+        self._p = self._thermostat.integrate_nhc(self._p, dt2)
+        self._p_eps = self._barostat.integrate_nhc_baro(self._p_eps, dt2)
+
+        self._update_atoms()
+
+    def get_conserved_energy(self) -> float:
+        """Return the conserved energy-like quantity.
+
+        This method is mainly used for testing.
+        """
+        conserved_energy = (
+            self.atoms.get_potential_energy(force_consistent=True)
+            + self.atoms.get_kinetic_energy()
+            + self._thermostat.get_thermostat_energy()
+            + self._barostat.get_barostat_energy()
+            + self._p_eps * self._p_eps / (2 * self._barostat.W)
+            + self._pressure_au * self._get_volume()
+        )
+        return float(conserved_energy)
+
+    def _update_atoms(self) -> None:
+        self.atoms.set_positions(self._q)
+        self.atoms.set_momenta(self._p)
+        cell = self._cell0 * np.exp(self._eps)
+        # Never set scale_atoms=True
+        self.atoms.set_cell(cell, scale_atoms=False)
+
+    def _get_volume(self) -> float:
+        return self._volume0 * np.exp(3 * self._eps)
+
+    def _get_forces(self) -> np.ndarray:
+        self._update_atoms()
+        return self.atoms.get_forces(md=True)
+
+    def _get_pressure(self) -> np.ndarray:
+        self._update_atoms()
+        stress = self.atoms.get_stress(voigt=False, include_ideal_gas=True)
+        pressure = -np.trace(stress) / 3
+        return pressure
+
+    def _integrate_q(self, delta: float) -> None:
+        """Integrate exp(i * L_1 * delta)"""
+        x = delta * self._p_eps / self._barostat.W
+        self._q = (
+            self._q * np.exp(x)
+            + self._p * delta / self.masses * exprel(x)
+        )
+
+    def _integrate_p(self, delta: float) -> None:
+        """Integrate exp(i * L_2 * delta)"""
+        x = (1 + 1 / self._num_atoms_global) * self._p_eps * delta \
+                / self._barostat.W
+        forces = self._get_forces()
+        self._p = self._p * np.exp(-x) + delta * forces * exprel(-x)
+
+    def _integrate_q_cell(self, delta: float) -> None:
+        """Integrate exp(i * L_(epsilon, 1) * delta)"""
+        self._eps += delta * self._p_eps / self._barostat.W
+
+    def _integrate_p_cell(self, delta: float) -> None:
+        """Integrate exp(i * L_(epsilon, 2) * delta)"""
+        pressure = self._get_pressure()
+        volume = self._get_volume()
+        G = (
+            3 * volume * (pressure - self._pressure_au)
+            + np.sum(self._p**2 / self.masses) / self._num_atoms_global
+        )
+        self._p_eps += delta * G
+
+
+class IsotropicMTKBarostat:
+    """MTK barostat for isotropic volume fluctuations.
+
+    See `IsotropicMTKNPT` for the references.
+    """
+    def __init__(
+        self,
+        num_atoms_global: int,
+        temperature_K: float,
+        pdamp: float,
+        pchain: int = 3,
+        ploop: int = 1,
+    ):
+        self._num_atoms_global = num_atoms_global
+        self._pdamp = pdamp
+        self._pchain = pchain
+        self._ploop = ploop
+
+        self._kT = ase.units.kB * temperature_K
+
+        self._W = (3 * self._num_atoms_global + 3) * self._kT * self._pdamp**2
+
+        assert pchain >= 1
+        self._R = np.zeros(self._pchain)
+        self._R[0] = 9 * self._kT * self._pdamp**2
+        self._R[1:] = self._kT * self._pdamp**2
+
+        self._xi = np.zeros(self._pchain)  # barostat coordinates
+        self._p_xi = np.zeros(self._pchain)
+
+    @property
+    def W(self) -> float:
+        """Virtual mass for barostat momenta `p_xi`."""
+        return self._W
+
+    def get_barostat_energy(self) -> float:
+        """Return energy-like contribution from the barostat variables."""
+        energy = (
+            + np.sum(0.5 * self._p_xi**2 / self._R)
+            + self._kT * np.sum(self._xi)
+        )
+        return float(energy)
 
-            if j == 0:
-                g_j = np.sum(p**2 / self._masses) \
-                    - 3 * self._num_atoms * self._kT
-            else:
-                g_j = self._p_eta[j - 1] ** 2 / self._Q[j - 1] - self._kT
-            self._p_eta[j] += delta2 * g_j
-
-            if j < self._tchain - 1:
-                self._p_eta[j] *= np.exp(
-                    -delta4 * self._p_eta[j + 1] / self._Q[j + 1]
+    def integrate_nhc_baro(self, p_eps: float, delta: float) -> float:
+        """Integrate exp(i * L_NHC-baro * delta)"""
+        for _ in range(self._ploop):
+            for coeff in FOURTH_ORDER_COEFFS:
+                p_eps = self._integrate_nhc_baro_loop(
+                    p_eps, coeff * delta / self._ploop
                 )
+        return p_eps
 
-        def _integrate_eta() -> None:
-            self._eta += delta * self._p_eta / self._Q
+    def _integrate_nhc_baro_loop(self, p_eps: float, delta: float) -> float:
+        delta2 = delta / 2
+        delta4 = delta / 4
 
-        def _integrate_nhc_p(p: np.ndarray) -> None:
-            p *= np.exp(-delta * self._p_eta[0] / self._Q[0])
+        for j in range(self._pchain):
+            self._integrate_p_xi_j(p_eps, self._pchain - j - 1, delta2, delta4)
+        self._integrate_xi(delta)
+        p_eps = self._integrate_nhc_p_eps(p_eps, delta)
+        for j in range(self._pchain):
+            self._integrate_p_xi_j(p_eps, j, delta2, delta4)
+
+        return p_eps
+
+    def _integrate_p_xi_j(self, p_eps: float, j: int,
+                          delta2: float, delta4: float) -> None:
+        if j < self._pchain - 1:
+            self._p_xi[j] *= np.exp(
+                -delta4 * self._p_xi[j + 1] / self._R[j + 1]
+            )
+
+        if j == 0:
+            g_j = p_eps ** 2 / self._W - self._kT
+        else:
+            g_j = self._p_xi[j - 1] ** 2 / self._R[j - 1] - self._kT
+        self._p_xi[j] += delta2 * g_j
+
+        if j < self._pchain - 1:
+            self._p_xi[j] *= np.exp(
+                -delta4 * self._p_xi[j + 1] / self._R[j + 1]
+            )
+
+    def _integrate_xi(self, delta: float) -> None:
+        self._xi += delta * self._p_xi / self._R
+
+    def _integrate_nhc_p_eps(self, p_eps: float, delta: float) -> float:
+        p_eps_new = p_eps * float(
+            np.exp(-delta * self._p_xi[0] / self._R[0])
+        )
+        return p_eps_new
 
-        for j in range(self._tchain):
-            _integrate_p_eta_j(p, self._tchain - j - 1)
-        _integrate_eta()
-        _integrate_nhc_p(p)
-        for j in range(self._tchain):
-            _integrate_p_eta_j(p, j)
 
-        return p
+class MTKNPT(MolecularDynamics):
+    """Isothermal-isobaric molecular dynamics with volume-and-cell fluctuations
+    by Martyna-Tobias-Klein (MTK) method [1].
+
+    See also `NoseHooverChainNVT` for the references.
+
+    - [1] G. J. Martyna, D. J. Tobias, and M. L. Klein, J. Chem. Phys. 101,
+          4177-4189 (1994). https://doi.org/10.1063/1.467468
+    """
+    def __init__(
+        self,
+        atoms: Atoms,
+        timestep: float,
+        temperature_K: float,
+        pressure_au: float,
+        tdamp: float,
+        pdamp: float,
+        tchain: int = 3,
+        pchain: int = 3,
+        tloop: int = 1,
+        ploop: int = 1,
+        **kwargs,
+    ):
+        """
+        Parameters
+        ----------
+        atoms: ase.Atoms
+            The atoms object.
+        timestep: float
+            The time step in ASE time units.
+        temperature_K: float
+            The target temperature in K.
+        pressure_au: float
+            The external pressure in eV/Ang^3.
+        tdamp: float
+            The characteristic time scale for the thermostat in ASE time units.
+            Typically, it is set to 100 times of `timestep`.
+        pdamp: float
+            The characteristic time scale for the barostat in ASE time units.
+            Typically, it is set to 1000 times of `timestep`.
+        tchain: int
+            The number of thermostat variables in the Nose-Hoover thermostat.
+        pchain: int
+            The number of barostat variables in the MTK barostat.
+        tloop: int
+            The number of sub-steps in thermostat integration.
+        ploop: int
+            The number of sub-steps in barostat integration.
+        **kwargs : dict, optional
+            Additional arguments passed to :class:~ase.md.md.MolecularDynamics
+            base class.
+        """
+        super().__init__(
+            atoms=atoms,
+            timestep=timestep,
+            **kwargs,
+        )
+        assert self.masses.shape == (len(self.atoms), 1)
+
+        if len(atoms.constraints) > 0:
+            raise NotImplementedError(
+                "Current implementation does not support constraints"
+            )
+
+        self._num_atoms_global = self.atoms.get_global_number_of_atoms()
+        self._thermostat = NoseHooverChainThermostat(
+            num_atoms_global=self._num_atoms_global,
+            masses=self.masses,
+            temperature_K=temperature_K,
+            tdamp=tdamp,
+            tchain=tchain,
+            tloop=tloop,
+        )
+        self._barostat = MTKBarostat(
+            num_atoms_global=self._num_atoms_global,
+            temperature_K=temperature_K,
+            pdamp=pdamp,
+            pchain=pchain,
+            ploop=ploop,
+        )
+
+        self._temperature_K = temperature_K
+        self._pressure_au = pressure_au
+
+        self._kT = ase.units.kB * self._temperature_K
+
+        # The following variables are updated during self.step()
+        self._q = self.atoms.get_positions()  # positions
+        self._p = self.atoms.get_momenta()  # momenta
+        self._h = np.array(self.atoms.get_cell())  # cell
+        self._p_g = np.zeros((3, 3))  # cell momenta
+
+    def step(self) -> None:
+        dt2 = self.dt / 2
+
+        self._p_g = self._barostat.integrate_nhc_baro(self._p_g, dt2)
+        self._p = self._thermostat.integrate_nhc(self._p, dt2)
+        self._integrate_p_cell(dt2)
+        self._integrate_p(dt2)
+        self._integrate_q(self.dt)
+        self._integrate_q_cell(self.dt)
+        self._integrate_p(dt2)
+        self._integrate_p_cell(dt2)
+        self._p = self._thermostat.integrate_nhc(self._p, dt2)
+        self._p_g = self._barostat.integrate_nhc_baro(self._p_g, dt2)
+
+        self._update_atoms()
+
+    def get_conserved_energy(self) -> float:
+        conserved_energy = (
+            self.atoms.get_total_energy()
+            + self._thermostat.get_thermostat_energy()
+            + self._barostat.get_barostat_energy()
+            + np.trace(self._p_g.T @ self._p_g) / (2 * self._barostat.W)
+            + self._pressure_au * self._get_volume()
+        )
+        return float(conserved_energy)
+
+    def _update_atoms(self) -> None:
+        self.atoms.set_positions(self._q)
+        self.atoms.set_momenta(self._p)
+        self.atoms.set_cell(self._h, scale_atoms=False)
+
+    def _get_volume(self) -> float:
+        return np.abs(np.linalg.det(self._h))
+
+    def _get_forces(self) -> np.ndarray:
+        self._update_atoms()
+        return self.atoms.get_forces(md=True)
+
+    def _get_stress(self) -> np.ndarray:
+        self._update_atoms()
+        stress = self.atoms.get_stress(voigt=False, include_ideal_gas=True)
+        return -stress
+
+    def _integrate_q(self, delta: float) -> None:
+        """Integrate exp(i * L_1 * delta)"""
+        # eigvals: (3-eigvec), U: (3-xyz, 3-eigvec)
+        eigvals, U = np.linalg.eigh(self._p_g)
+        x = self._q @ U  # (num_atoms, 3-eigvec)
+        y = self._p @ U  # (num_atoms, 3-eigvec)
+        sol = (
+            x * np.exp(eigvals * delta / self._barostat.W)[None, :]
+            + delta * y / self.masses * exprel(
+                eigvals * delta / self._barostat.W
+            )[None, :]
+        )  # (num_atoms, 3-eigvec)
+        self._q = sol @ U.T
+
+    def _integrate_p(self, delta: float) -> None:
+        """Integrate exp(i * L_2 * delta)"""
+        forces = self._get_forces()  # (num_atoms, 3-xyz)
+
+        # eigvals: (3-eigvec), U: (3-xyz, 3-eigvec)
+        eigvals, U = np.linalg.eigh(self._p_g)
+        kappas = eigvals \
+            + np.trace(self._p_g) / (3 * self._num_atoms_global)  # (3-eigvec)
+        y = self._p @ U  # (num_atoms, 3-eigvec)
+        sol = (
+            y * np.exp(-kappas * delta / self._barostat.W)[None, :]
+            + delta * (forces @ U) * exprel(
+                -kappas * delta / self._barostat.W
+            )[None, :]
+        )  # (num_atoms, 3-eigvec)
+        self._p = sol @ U.T
+
+    def _integrate_q_cell(self, delta: float) -> None:
+        """Integrate exp(i * L_(g, 1) * delta)"""
+        # U @ np.diag(eigvals) @ U.T = self._p_g
+        # eigvals: (3-eigvec), U: (3-xyz, 3-eigvec)
+        eigvals, U = np.linalg.eigh(self._p_g)
+        n = self._h @ U  # (3-axis, 3-eigvec)
+        sol = n * np.exp(
+            eigvals * delta / self._barostat.W
+        )[None, :]  # (3-axis, 3-eigvec)
+        self._h = sol @ U.T
+
+    def _integrate_p_cell(self, delta: float) -> None:
+        """Integrate exp(i * L_(g, 2) * delta)"""
+        stress = self._get_stress()
+        G = (
+            self._get_volume() * (stress - self._pressure_au * np.eye(3))
+            + np.sum(self._p**2 / self.masses) / (3 * self._num_atoms_global)
+                * np.eye(3)
+        )
+        self._p_g += delta * G
+
+
+class MTKBarostat:
+    """MTK barostat for volume-and-cell fluctuations.
+
+    See `MTKNPT` for the references.
+    """
+    def __init__(
+        self,
+        num_atoms_global: int,
+        temperature_K: float,
+        pdamp: float,
+        pchain: int = 3,
+        ploop: int = 1,
+    ):
+        self._num_atoms_global = num_atoms_global
+        self._pdamp = pdamp
+        self._pchain = pchain
+        self._ploop = ploop
+
+        self._kT = ase.units.kB * temperature_K
+
+        self._W = (self._num_atoms_global + 1) * self._kT * self._pdamp**2
+
+        assert pchain >= 1
+        self._R = np.zeros(self._pchain)
+        cell_dof = 9  # TODO:
+        self._R[0] = cell_dof * self._kT * self._pdamp**2
+        self._R[1:] = self._kT * self._pdamp**2
+
+        self._xi = np.zeros(self._pchain)  # barostat coordinates
+        self._p_xi = np.zeros(self._pchain)
+
+    @property
+    def W(self) -> float:
+        return self._W
+
+    def get_barostat_energy(self) -> float:
+        energy = (
+            np.sum(self._p_xi**2 / self._R) / 2
+            + 9 * self._kT * self._xi[0]
+            + self._kT * np.sum(self._xi[1:])
+        )
+        return float(energy)
+
+    def integrate_nhc_baro(self, p_g: np.ndarray, delta: float) -> np.ndarray:
+        """Integrate exp(i * L_NHC-baro * delta)"""
+        for _ in range(self._ploop):
+            for coeff in FOURTH_ORDER_COEFFS:
+                p_g = self._integrate_nhc_baro_loop(
+                    p_g, coeff * delta / self._ploop
+                )
+        return p_g
+
+    def _integrate_nhc_baro_loop(
+        self, p_g: np.ndarray, delta: float
+    ) -> np.ndarray:
+        delta2 = delta / 2
+        delta4 = delta / 4
+
+        for j in range(self._pchain):
+            self._integrate_p_xi_j(p_g, self._pchain - j - 1, delta2, delta4)
+        self._integrate_xi(delta)
+        self._integrate_nhc_p_eps(p_g, delta)
+        for j in range(self._pchain):
+            self._integrate_p_xi_j(p_g, j, delta2, delta4)
+
+        return p_g
+
+    def _integrate_p_xi_j(
+        self, p_g: np.ndarray, j: int, delta2: float, delta4: float
+    ) -> None:
+        if j < self._pchain - 1:
+            self._p_xi[j] *= np.exp(
+                -delta4 * self._p_xi[j + 1] / self._R[j + 1]
+            )
+
+        if j == 0:
+            # TODO: do we need to substitute 9 with cell_dof?
+            g_j = np.trace(p_g.T @ p_g) / self._W - 9 * self._kT
+        else:
+            g_j = self._p_xi[j - 1] ** 2 / self._R[j - 1] - self._kT
+        self._p_xi[j] += delta2 * g_j
+
+        if j < self._pchain - 1:
+            self._p_xi[j] *= np.exp(
+                -delta4 * self._p_xi[j + 1] / self._R[j + 1]
+            )
+
+    def _integrate_xi(self, delta: float) -> None:
+        for j in range(self._pchain):
+            self._xi[j] += delta * self._p_xi[j] / self._R[j]
+
+    def _integrate_nhc_p_eps(self, p_g: np.ndarray, delta: float) -> None:
+        p_g *= np.exp(-delta * self._p_xi[0] / self._R[0])
diff -pruN 3.24.0-1/ase/md/npt.py 3.26.0-1/ase/md/npt.py
--- 3.24.0-1/ase/md/npt.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/npt.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,5 +1,9 @@
+# fmt: off
+
 '''Constant pressure/stress and temperature dynamics.
 
+**This dynamics is not recommended due to stability problems.**
+
 Combined Nose-Hoover and Parrinello-Rahman dynamics, creating an NPT
 (or N,stress,T) ensemble.
 
@@ -246,7 +250,7 @@ class NPT(MolecularDynamics):
         self.frac_traceless = fracTraceless
 
     def get_strain_rate(self):
-        """Get the strain rate as an upper-triangular 3x3 matrix.
+        """Get the strain rate as a triangular 3x3 matrix.
 
         This includes the fluctuations in the shape of the computational box.
 
@@ -254,14 +258,14 @@ class NPT(MolecularDynamics):
         return np.array(self.eta, copy=1)
 
     def set_strain_rate(self, rate):
-        """Set the strain rate.  Must be an upper triangular 3x3 matrix.
+        """Set the strain rate.  Must be a triangular 3x3 matrix.
 
         If you set a strain rate along a direction that is "masked out"
         (see ``set_mask``), the strain rate along that direction will be
         maintained constantly.
         """
-        if not (rate.shape == (3, 3) and self._isuppertriangular(rate)):
-            raise ValueError("Strain rate must be an upper triangular matrix.")
+        if not (rate.shape == (3, 3) and self._triangular(rate)):
+            raise ValueError("Strain rate must be a triangular matrix.")
         self.eta = rate
         if self.initialized:
             # Recalculate h_past and eta_past so they match the current value.
@@ -271,8 +275,20 @@ class NPT(MolecularDynamics):
         "Get the elapsed time."
         return self.timeelapsed
 
-    def run(self, steps):
-        """Perform a number of time steps."""
+    def irun(self, steps):
+        """Run dynamics algorithm as generator.
+
+        Parameters
+        ----------
+        steps : int
+            Number of dynamics steps to be run.
+
+        Yields
+        ------
+        complete : bool
+            True if the maximum number of steps are reached.
+        """
+
         if not self.initialized:
             self.initialize()
         else:
@@ -280,10 +296,25 @@ class NPT(MolecularDynamics):
                 raise NotImplementedError(
                     "You have modified the atoms since the last timestep.")
 
-        for _ in range(steps):
-            self.step()
-            self.nsteps += 1
-            self.call_observers()
+        yield from super().irun(steps)
+
+    def run(self, steps):
+        """Perform a number of time steps.
+
+        Parameters
+        ----------
+        steps : int
+            Number of dynamics steps to be run.
+
+        Yields
+        ------
+        complete : bool
+            True if the maximum number of steps are reached.
+        """
+
+        for complete in self.irun(steps):
+            pass
+        return complete
 
     def have_the_atoms_been_changed(self):
         "Checks if the user has modified the positions or momenta of the atoms"
@@ -327,10 +358,10 @@ class NPT(MolecularDynamics):
 
         if self.frac_traceless == 1:
             eta_future = self.eta_past + self.mask * \
-                self._makeuppertriangular(deltaeta)
+                self._maketriangular(deltaeta)
         else:
             trace_part, traceless_part = self._separatetrace(
-                self._makeuppertriangular(deltaeta))
+                self._maketriangular(deltaeta))
             eta_future = (self.eta_past + trace_part +
                           self.frac_traceless * traceless_part)
 
@@ -378,15 +409,10 @@ class NPT(MolecularDynamics):
         dt = self.dt
         atoms = self.atoms
         self.h = self._getbox()
-        if not self._isuppertriangular(self.h):
-            print("I am", self)
-            print("self.h:")
-            print(self.h)
-            print("Min:", min((self.h[1, 0], self.h[2, 0], self.h[2, 1])))
-            print("Max:", max((self.h[1, 0], self.h[2, 0], self.h[2, 1])))
+        if not self._istriangular(self.h):
             raise NotImplementedError(
-                "Can (so far) only operate on lists of atoms where the "
-                "computational box is an upper triangular matrix.")
+                f"Can (so far) only operate on lists of atoms where the "
+                f"computational box is a triangular matrix. {self.h}")
         self.inv_h = linalg.inv(self.h)
         # The contents of the q arrays should migrate in parallel simulations.
         # self._make_special_q_arrays()
@@ -622,24 +648,40 @@ class NPT(MolecularDynamics):
                         * (self.stresscalculator() - self.externalstress))
         if self.frac_traceless == 1:
             self.eta_past = self.eta - self.mask * \
-                self._makeuppertriangular(deltaeta)
+                self._maketriangular(deltaeta)
         else:
             trace_part, traceless_part = self._separatetrace(
-                self._makeuppertriangular(deltaeta))
+                self._maketriangular(deltaeta))
             self.eta_past = (self.eta - trace_part -
                              self.frac_traceless * traceless_part)
 
-    def _makeuppertriangular(self, sixvector):
-        "Make an upper triangular matrix from a 6-vector."
-        return np.array(((sixvector[0], sixvector[5], sixvector[4]),
-                         (0, sixvector[1], sixvector[3]),
-                         (0, 0, sixvector[2])))
-
     @staticmethod
     def _isuppertriangular(m) -> bool:
-        "Check that a matrix is on upper triangular form."
+        "Check that a 3x3 matrix is of upper triangular form."
         return m[1, 0] == m[2, 0] == m[2, 1] == 0.0
 
+    @classmethod
+    def _islowertriangular(cls, m) -> bool:
+        "Check that a 3x3 matrix is of lower triangular form."
+        return cls._isuppertriangular(m.T)
+
+    @classmethod
+    def _istriangular(cls, m) -> bool:
+        "Check that a 3x3 matrix is of triangular form."
+        return cls._isuppertriangular(m) or cls._islowertriangular(m)
+
+    def _maketriangular(self, sixvector):
+        "Make 3x3 triangular matrix from a 6-vector."
+        if self._isuppertriangular(self.h):
+            return np.array(((sixvector[0], sixvector[5], sixvector[4]),
+                            (0, sixvector[1], sixvector[3]),
+                            (0, 0, sixvector[2])))
+
+        if self._islowertriangular(self.h):
+            return np.array(((sixvector[0], 0, 0),
+                            (sixvector[5], sixvector[1], 0),
+                            (sixvector[4], sixvector[3], sixvector[2])))
+
     def _calculateconstants(self):
         """(Re)calculate some constants when pfactor,
         ttime or temperature have been changed."""
diff -pruN 3.24.0-1/ase/md/nptberendsen.py 3.26.0-1/ase/md/nptberendsen.py
--- 3.24.0-1/ase/md/nptberendsen.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/nptberendsen.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Berendsen NPT dynamics class."""
 import warnings
 from typing import IO, Optional, Union
diff -pruN 3.24.0-1/ase/md/nvtberendsen.py 3.26.0-1/ase/md/nvtberendsen.py
--- 3.24.0-1/ase/md/nvtberendsen.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/nvtberendsen.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,11 +1,12 @@
 """Berendsen NVT dynamics class."""
-from typing import IO, Optional, Union
+
+import warnings
+from typing import Optional
 
 import numpy as np
 
 from ase import Atoms
 from ase.md.md import MolecularDynamics
-from ase.parallel import world
 
 
 class NVTBerendsen(MolecularDynamics):
@@ -18,16 +19,12 @@ class NVTBerendsen(MolecularDynamics):
         fixcm: bool = True,
         *,
         temperature_K: Optional[float] = None,
-        trajectory: Optional[str] = None,
-        logfile: Optional[Union[IO, str]] = None,
-        loginterval: int = 1,
-        communicator=world,
-        append_trajectory: bool = False,
+        **kwargs,
     ):
         """Berendsen (constant N, V, T) molecular dynamics.
 
-        Parameters:
-
+        Parameters
+        ----------
         atoms: Atoms object
             The list of atoms.
 
@@ -48,38 +45,29 @@ class NVTBerendsen(MolecularDynamics):
             If True, the position and momentum of the center of mass is
             kept unperturbed.  Default: True.
 
-        trajectory: Trajectory object or str (optional)
-            Attach trajectory object.  If *trajectory* is a string a
-            Trajectory will be constructed.  Use *None* for no
-            trajectory.
-
-        logfile: file object or str (optional)
-            If *logfile* is a string, a file with that name will be opened.
-            Use '-' for stdout.
-
-        loginterval: int (optional)
-            Only write a log line for every *loginterval* time steps.
-            Default: 1
-
-        append_trajectory: boolean (optional)
-            Defaults to False, which causes the trajectory file to be
-            overwriten each time the dynamics is restarted from scratch.
-            If True, the new structures are appended to the trajectory
-            file instead.
+        **kwargs : dict, optional
+            Additional arguments passed to :class:~ase.md.md.MolecularDynamics
+            base class.
 
         """
+        if 'communicator' in kwargs:
+            msg = (
+                '`communicator` has been deprecated since ASE 3.25.0 '
+                'and will be removed in ASE 3.26.0. Use `comm` instead.'
+            )
+            warnings.warn(msg, FutureWarning)
+            kwargs['comm'] = kwargs.pop('communicator')
+
+        MolecularDynamics.__init__(self, atoms, timestep, **kwargs)
 
-        MolecularDynamics.__init__(self, atoms, timestep, trajectory,
-                                   logfile, loginterval,
-                                   append_trajectory=append_trajectory)
         if taut is None:
             raise TypeError("Missing 'taut' argument.")
         self.taut = taut
-        self.temperature = self._process_temperature(temperature,
-                                                     temperature_K, 'K')
+        self.temperature = self._process_temperature(
+            temperature, temperature_K, 'K'
+        )
 
         self.fix_com = fixcm  # will the center of mass be held fixed?
-        self.communicator = communicator
 
     def set_taut(self, taut):
         self.taut = taut
@@ -88,8 +76,9 @@ class NVTBerendsen(MolecularDynamics):
         return self.taut
 
     def set_temperature(self, temperature=None, *, temperature_K=None):
-        self.temperature = self._process_temperature(temperature,
-                                                     temperature_K, 'K')
+        self.temperature = self._process_temperature(
+            temperature, temperature_K, 'K'
+        )
 
     def get_temperature(self):
         return self.temperature
@@ -101,13 +90,13 @@ class NVTBerendsen(MolecularDynamics):
         return self.dt
 
     def scale_velocities(self):
-        """ Do the NVT Berendsen velocity scaling """
+        """Do the NVT Berendsen velocity scaling"""
         tautscl = self.dt / self.taut
         old_temperature = self.atoms.get_temperature()
 
-        scl_temperature = np.sqrt(1.0 +
-                                  (self.temperature / old_temperature - 1.0) *
-                                  tautscl)
+        scl_temperature = np.sqrt(
+            1.0 + (self.temperature / old_temperature - 1.0) * tautscl
+        )
         # Limit the velocity scaling to reasonable values
         if scl_temperature > 1.1:
             scl_temperature = 1.1
@@ -139,8 +128,9 @@ class NVTBerendsen(MolecularDynamics):
             p = p - psum
 
         self.atoms.set_positions(
-            self.atoms.get_positions() +
-            self.dt * p / self.atoms.get_masses()[:, np.newaxis])
+            self.atoms.get_positions()
+            + self.dt * p / self.atoms.get_masses()[:, np.newaxis]
+        )
 
         # We need to store the momenta on the atoms before calculating
         # the forces, as in a parallel Asap calculation atoms may
diff -pruN 3.24.0-1/ase/md/switch_langevin.py 3.26.0-1/ase/md/switch_langevin.py
--- 3.24.0-1/ase/md/switch_langevin.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/switch_langevin.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import Any, List, Optional
 
 import numpy as np
diff -pruN 3.24.0-1/ase/md/velocitydistribution.py 3.26.0-1/ase/md/velocitydistribution.py
--- 3.24.0-1/ase/md/velocitydistribution.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/velocitydistribution.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,5 +1,3 @@
-# VelocityDistributions.py -- set up a velocity distribution
-
 """Module for setting up velocity distributions such as Maxwell–Boltzmann.
 
 Currently, only a few functions are defined, such as
@@ -7,13 +5,15 @@ MaxwellBoltzmannDistribution, which sets
 atoms according to a Maxwell-Boltzmann distribution at a given
 temperature.
 """
+
+import warnings
 from typing import Literal, Optional
 
 import numpy as np
 
 from ase import Atoms, units
 from ase.md.md import process_temperature
-from ase.parallel import world
+from ase.parallel import DummyMPI, world
 
 # define a ``zero'' temperature to avoid divisions by zero
 eps_temp = 1e-12
@@ -23,9 +23,11 @@ class UnitError(Exception):
     """Exception raised when wrong units are specified"""
 
 
-def force_temperature(atoms: Atoms,
-                      temperature: float,
-                      unit: Literal["K", "eV"] = "K"):
+def force_temperature(
+    atoms: Atoms,
+    temperature: float,
+    unit: Literal['K', 'eV'] = 'K',
+):
     """
     Force the temperature of the atomic system to a precise value.
 
@@ -45,9 +47,9 @@ def force_temperature(atoms: Atoms,
         Can be either 'K' (Kelvin) or 'eV' (electron volts). Default is 'K'.
     """
 
-    if unit == "K":
+    if unit == 'K':
         target_temp = temperature * units.kB
-    elif unit == "eV":
+    elif unit == 'eV':
         target_temp = temperature
     else:
         raise UnitError(f"'{unit}' is not supported, use 'K' or 'eV'.")
@@ -62,36 +64,32 @@ def force_temperature(atoms: Atoms,
     atoms.set_momenta(atoms.get_momenta() * np.sqrt(scale))
 
 
-def _maxwellboltzmanndistribution(masses, temp, communicator=None, rng=None):
+def _maxwellboltzmanndistribution(masses, temp, comm=world, rng=None):
     """Return a Maxwell-Boltzmann distribution with a given temperature.
 
-    Paremeters:
-
+    Parameters
+    ----------
     masses: float
         The atomic masses.
 
     temp: float
         The temperature in electron volt.
 
-    communicator: MPI communicator (optional)
-        Communicator used to distribute an identical distribution to
-        all tasks.  Set to 'serial' to disable communication (setting to None
-        gives the default).  Default: ase.parallel.world
+    comm: MPI communicator (optional, default: ase.parallel.world)
+        Communicator used to distribute an identical distribution to all tasks.
 
     rng: numpy RNG (optional)
         The random number generator.  Default: np.random
 
-    Returns:
-
-    A numpy array with Maxwell-Boltzmann distributed momenta.
+    Returns
+    -------
+    np.ndarray
+        Maxwell-Boltzmann distributed momenta.
     """
     if rng is None:
         rng = np.random
-    if communicator is None:
-        communicator = world
     xi = rng.standard_normal((len(masses), 3))
-    if communicator != 'serial':
-        communicator.broadcast(xi, 0)
+    comm.broadcast(xi, 0)
     momenta = xi * np.sqrt(masses * temp)[:, np.newaxis]
     return momenta
 
@@ -101,27 +99,34 @@ def MaxwellBoltzmannDistribution(
     temp: Optional[float] = None,
     *,
     temperature_K: Optional[float] = None,
+    comm=world,
     communicator=None,
     force_temp: bool = False,
     rng=None,
 ):
     """Set the atomic momenta to a Maxwell-Boltzmann distribution.
 
-    Parameters:
-
+    Parameters
+    ----------
     atoms: Atoms object
         The atoms.  Their momenta will be modified.
 
     temp: float (deprecated)
-        The temperature in eV.  Deprecated, use temperature_K instead.
+        The temperature in eV.  Deprecated, use ``temperature_K`` instead.
 
     temperature_K: float
         The temperature in Kelvin.
 
-    communicator: MPI communicator (optional)
-        Communicator used to distribute an identical distribution to
-        all tasks.  Set to 'serial' to disable communication.  Leave as None to
-        get the default: ase.parallel.world
+    comm: MPI communicator, default: :data:`ase.parallel.world`
+        Communicator used to distribute an identical distribution to all tasks.
+
+        .. versionadded:: 3.26.0
+
+    communicator
+
+        .. deprecated:: 3.26.0
+
+           To be removed in ASE 3.27.0 in favor of ``comm``.
 
     force_temp: bool (optional, default: False)
         If True, the random momenta are rescaled so the kinetic energy is
@@ -131,15 +136,29 @@ def MaxwellBoltzmannDistribution(
     rng: Numpy RNG (optional)
         Random number generator.  Default: numpy.random
         If you would like to always get the identical distribution, you can
-        supply a random seed like `rng=numpy.random.RandomState(seed)`, where
+        supply a random seed like ``rng=numpy.random.RandomState(seed)``, where
         seed is an integer.
     """
+    if communicator is not None:
+        msg = (
+            '`communicator` has been deprecated since ASE 3.26.0 '
+            'and will be removed in ASE 3.27.0. Use `comm` instead.'
+        )
+        warnings.warn(msg, FutureWarning)
+        comm = communicator
+    if comm == 'serial':
+        msg = (
+            '`comm=="serial"` has been deprecated since ASE 3.26.0 '
+            'and will be removed in ASE 3.27.0. Use `comm=DummyMPI()` instead.'
+        )
+        warnings.warn(msg, FutureWarning)
+        comm = DummyMPI()
     temp = units.kB * process_temperature(temp, temperature_K, 'eV')
     masses = atoms.get_masses()
-    momenta = _maxwellboltzmanndistribution(masses, temp, communicator, rng)
+    momenta = _maxwellboltzmanndistribution(masses, temp, comm=comm, rng=rng)
     atoms.set_momenta(momenta)
     if force_temp:
-        force_temperature(atoms, temperature=temp, unit="eV")
+        force_temperature(atoms, temperature=temp, unit='eV')
 
 
 def Stationary(atoms: Atoms, preserve_temperature: bool = True):
@@ -300,7 +319,7 @@ def phonon_harmonics(
     temp = units.kB * process_temperature(temp, temperature_K, 'eV')
 
     # Build dynamical matrix
-    rminv = (masses ** -0.5).repeat(3)
+    rminv = (masses**-0.5).repeat(3)
     dynamical_matrix = force_constants * rminv[:, None] * rminv[None, :]
 
     # Solve eigenvalue problem to compute phonon spectrum and eigenvectors
@@ -311,13 +330,13 @@ def phonon_harmonics(
         zeros = w2_s[:3]
         worst_zero = np.abs(zeros).max()
         if worst_zero > 1e-3:
-            msg = "Translational deviate from 0 significantly: "
-            raise ValueError(msg + f"{w2_s[:3]}")
+            msg = 'Translational deviate from 0 significantly: '
+            raise ValueError(msg + f'{w2_s[:3]}')
 
         w2min = w2_s[3:].min()
         if w2min < 0:
-            msg = "Dynamical matrix has negative eigenvalues such as "
-            raise ValueError(msg + f"{w2min}")
+            msg = 'Dynamical matrix has negative eigenvalues such as '
+            raise ValueError(msg + f'{w2min}')
 
     # First three modes are translational so ignore:
     nw = len(w2_s) - 3
diff -pruN 3.24.0-1/ase/md/verlet.py 3.26.0-1/ase/md/verlet.py
--- 3.24.0-1/ase/md/verlet.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/md/verlet.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Velocity Verlet."""
 from ase.md.md import MolecularDynamics
 
diff -pruN 3.24.0-1/ase/mep/__init__.py 3.26.0-1/ase/mep/__init__.py
--- 3.24.0-1/ase/mep/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/mep/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Methods for finding minimum-energy paths and/or saddle points."""
 
 from ase.mep.autoneb import AutoNEB
diff -pruN 3.24.0-1/ase/mep/autoneb.py 3.26.0-1/ase/mep/autoneb.py
--- 3.24.0-1/ase/mep/autoneb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/mep/autoneb.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import shutil
 import types
diff -pruN 3.24.0-1/ase/mep/dimer.py 3.26.0-1/ase/mep/dimer.py
--- 3.24.0-1/ase/mep/dimer.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/mep/dimer.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Minimum mode follower for finding saddle points in an unbiased way.
 
 There is, currently, only one implemented method: The Dimer method.
@@ -1076,10 +1078,9 @@ class MinModeTranslate(Optimizer):
         self.direction_old = direction.copy()
         return self.cg_direction.copy()
 
-    def log(self, f=None, stepsize=None):
+    def log(self, gradient, stepsize=None):
         """Log each step of the optimization."""
-        if f is None:
-            f = self.dimeratoms.get_forces()
+        f = self.dimeratoms.get_forces()
         if self.logfile is not None:
             T = time.localtime()
             e = self.dimeratoms.get_potential_energy()
diff -pruN 3.24.0-1/ase/mep/neb.py 3.26.0-1/ase/mep/neb.py
--- 3.24.0-1/ase/mep/neb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/mep/neb.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,8 +1,11 @@
+# fmt: off
+
 import sys
 import threading
 import time
 import warnings
 from abc import ABC, abstractmethod
+from functools import cached_property
 
 import numpy as np
 from scipy.integrate import cumulative_trapezoid
@@ -18,7 +21,7 @@ from ase.optimize.ode import ode12r
 from ase.optimize.optimize import DEFAULT_MAX_STEPS, Optimizer
 from ase.optimize.precon import Precon, PreconImages
 from ase.optimize.sciopt import OptimizerConvergenceError
-from ase.utils import deprecated, lazyproperty
+from ase.utils import deprecated
 from ase.utils.abc import Optimizable
 from ase.utils.forcecurve import fit_images
 
@@ -38,11 +41,11 @@ class Spring:
         mic, _ = find_mic(pos2 - pos1, self.atoms1.cell, self.atoms1.pbc)
         return mic
 
-    @lazyproperty
+    @cached_property
     def t(self):
         return self._find_mic()
 
-    @lazyproperty
+    @cached_property
     def nt(self):
         return np.linalg.norm(self.t)
 
@@ -58,7 +61,7 @@ class NEBState:
                       self.energies[i], self.energies[i + 1],
                       self.neb.k[i])
 
-    @lazyproperty
+    @cached_property
     def imax(self):
         return 1 + np.argsort(self.energies[1:-1])[-1]
 
@@ -66,7 +69,7 @@ class NEBState:
     def emax(self):
         return self.energies[self.imax]
 
-    @lazyproperty
+    @cached_property
     def eqlength(self):
         images = self.images
         beeline = (images[self.neb.nimages - 1].get_positions() -
@@ -74,7 +77,7 @@ class NEBState:
         beelinelength = np.linalg.norm(beeline)
         return beelinelength / (self.neb.nimages - 1)
 
-    @lazyproperty
+    @cached_property
     def nimages(self):
         return len(self.images)
 
@@ -261,23 +264,20 @@ class NEBOptimizable(Optimizable):
     def __init__(self, neb):
         self.neb = neb
 
-    def get_forces(self):
-        return self.neb.get_forces()
+    def get_gradient(self):
+        return self.neb.get_forces().ravel()
 
-    def get_potential_energy(self):
+    def get_value(self):
         return self.neb.get_potential_energy()
 
-    def is_neb(self):
-        return True
-
-    def get_positions(self):
-        return self.neb.get_positions()
+    def get_x(self):
+        return self.neb.get_positions().ravel()
 
-    def set_positions(self, positions):
-        self.neb.set_positions(positions)
+    def set_x(self, x):
+        self.neb.set_positions(x.reshape(-1, 3))
 
-    def __len__(self):
-        return len(self.neb)
+    def ndofs(self):
+        return 3 * len(self.neb)
 
     def iterimages(self):
         return self.neb.iterimages()
@@ -1063,16 +1063,14 @@ def interpolate(images, mic=False, inter
                 unconstrained_image.set_positions(new_pos,
                                                   apply_constraint=False)
                 images[i].set_positions(new_pos, apply_constraint=True)
-                try:
-                    np.testing.assert_allclose(unconstrained_image.positions,
-                                               images[i].positions)
-                except AssertionError:
-                    raise RuntimeError(f"Constraint(s) in image number {i} \n"
+                if not np.allclose(unconstrained_image.positions,
+                                   images[i].positions):
+                    raise RuntimeError(f"Constraints in image {i}\n"
                                        "affect the interpolation results.\n"
-                                       "Please specify if you want to \n"
-                                       "apply or ignore the constraints \n"
-                                       "during the interpolation \n"
-                                       "with apply_constraint argument.")
+                                       "Please specify if you want to\n"
+                                       "apply or ignore the constraints\n"
+                                       "during the interpolation\n"
+                                       "with the apply_constraint argument.")
             else:
                 images[i].set_positions(new_pos,
                                         apply_constraint=apply_constraint)
diff -pruN 3.24.0-1/ase/neighborlist.py 3.26.0-1/ase/neighborlist.py
--- 3.24.0-1/ase/neighborlist.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/neighborlist.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import itertools
 
 import numpy as np
@@ -532,8 +534,8 @@ def neighbor_list(quantities, a, cutoff,
     The neighbor list is sorted by first atom index 'i', but not by second
     atom index 'j'.
 
-    Parameters:
-
+    Parameters
+    ----------
     quantities: str
         Quantities to compute by the neighbor list algorithm. Each character
         in this string defines a quantity. They are returned in a tuple of
@@ -569,67 +571,75 @@ def neighbor_list(quantities, a, cutoff,
         Maximum number of bins used in neighbor search. This is used to limit
         the maximum amount of memory required by the neighbor list.
 
-    Returns:
-
+    Returns
+    -------
     i, j, ...: array
         Tuple with arrays for each quantity specified above. Indices in `i`
         are returned in ascending order 0..len(a), but the order of (i,j)
         pairs is not guaranteed.
 
-    Examples:
-
-    Examples assume Atoms object *a* and numpy imported as *np*.
+    Examples
+    --------
 
-    1. Coordination counting::
+    >>> import numpy as np
+    >>> from ase.build import bulk, molecule
 
-        i = neighbor_list('i', a, 1.85)
-        coord = np.bincount(i)
+    1. Coordination counting
 
-    2. Coordination counting with different cutoffs for each pair of species::
-
-        i = neighbor_list('i', a,
-                          {('H', 'H'): 1.1, ('C', 'H'): 1.3, ('C', 'C'): 1.85})
-        coord = np.bincount(i)
-
-    3. Pair distribution function::
-
-        d = neighbor_list('d', a, 10.00)
-        h, bin_edges = np.histogram(d, bins=100)
-        pdf = h/(4*np.pi/3*(
-            bin_edges[1:]**3 - bin_edges[:-1]**3)) * a.get_volume()/len(a)
-
-    4. Pair potential::
-
-        i, j, d, D = neighbor_list('ijdD', a, 5.0)
-        energy = (-C/d**6).sum()
-        forces = (6*C/d**5  * (D/d).T).T
-        forces_x = np.bincount(j, weights=forces[:, 0], minlength=len(a)) - \
-                   np.bincount(i, weights=forces[:, 0], minlength=len(a))
-        forces_y = np.bincount(j, weights=forces[:, 1], minlength=len(a)) - \
-                   np.bincount(i, weights=forces[:, 1], minlength=len(a))
-        forces_z = np.bincount(j, weights=forces[:, 2], minlength=len(a)) - \
-                   np.bincount(i, weights=pair_forces[:, 2], minlength=len(a))
-
-    5. Dynamical matrix for a pair potential stored in a block sparse format::
-
-        from scipy.sparse import bsr_matrix
-        i, j, dr, abs_dr = neighbor_list('ijDd', atoms)
-        energy = (dr.T / abs_dr).T
-        dynmat = -(dde * (energy.reshape(-1, 3, 1)
-                   * energy.reshape(-1, 1, 3)).T).T \
-                 -(de / abs_dr * (np.eye(3, dtype=energy.dtype) - \
-                   (energy.reshape(-1, 3, 1) * energy.reshape(-1, 1, 3))).T).T
-        dynmat_bsr = bsr_matrix((dynmat, j, first_i),
-                                shape=(3*len(a), 3*len(a)))
-
-        dynmat_diag = np.empty((len(a), 3, 3))
-        for x in range(3):
-            for y in range(3):
-                dynmat_diag[:, x, y] = -np.bincount(i, weights=dynmat[:, x, y])
-
-        dynmat_bsr += bsr_matrix((dynmat_diag, np.arange(len(a)),
-                                  np.arange(len(a) + 1)),
-                                 shape=(3 * len(a), 3 * len(a)))
+    >>> atoms = molecule('isobutane')
+    >>> i = neighbor_list('i', atoms, 1.85)
+    >>> coord = np.bincount(i, minlength=len(atoms))
+
+    2. Coordination counting with different cutoffs for each pair of species
+
+    >>> cutoff = {('H', 'H'): 1.1, ('C', 'H'): 1.3, ('C', 'C'): 1.85}
+    >>> i = neighbor_list('i', atoms, cutoff)
+    >>> coord = np.bincount(i, minlength=len(atoms))
+
+    3. Pair distribution function
+
+    >>> atoms = bulk('Cu', cubic=True) * 3
+    >>> atoms.rattle(0.5, rng=np.random.default_rng(42))
+    >>> cutoff = 5.0
+    >>> d = neighbor_list('d', atoms, cutoff)
+    >>> hist, bin_edges = np.histogram(d, bins=100, range=(0.0, cutoff))
+    >>> hist = hist / len(atoms)  # per atom
+    >>> rho_mean = len(atoms) / atoms.cell.volume
+    >>> dv = 4.0 * np.pi * (bin_edges[1:] ** 3 - bin_edges[:-1] ** 3) / 3.0
+    >>> rho = hist / dv
+    >>> pdf = rho / rho_mean
+
+    4. Forces of a pair potential
+
+    >>> natoms = len(atoms)
+    >>> i, j, d, D = neighbor_list('ijdD', atoms, 5.0)
+    >>> # Lennard-Jones potential
+    >>> eps = 1.0
+    >>> sgm = 1.0
+    >>> epairs = 4.0 * eps * ((sgm / d) ** 12 - (sgm / d) ** 6)
+    >>> energy = 0.5 * epairs.sum()  # correct double-counting
+    >>> dd = -4.0 * eps * (12 * (sgm / d) ** 13 - 6 * (sgm / d) ** 7) / sgm
+    >>> dd = (dd * (D.T / d)).T
+    >>> fx = -1.0 * np.bincount(i, weights=dd[:, 0], minlength=natoms)
+    >>> fy = -1.0 * np.bincount(i, weights=dd[:, 1], minlength=natoms)
+    >>> fz = -1.0 * np.bincount(i, weights=dd[:, 2], minlength=natoms)
+
+    5. Force-constant matrix of a pair potential
+
+    >>> i, j, d, D = neighbor_list('ijdD', atoms, 5.0)
+    >>> epairs = 1.0 / d  # Coulomb potential
+    >>> forces = (D.T / d**3).T  # (npairs, 3)
+    >>> # second derivative
+    >>> d2 = 3.0 * D[:, :, None] * D[:, None, :] / d[:, None, None] ** 5
+    >>> for k in range(3):
+    ...     d2[:, k, k] -= 1.0 / d**3
+    >>> # force-constant matrix
+    >>> fc = np.zeros((natoms, 3, natoms, 3))
+    >>> for ia in range(natoms):
+    ...     for ja in range(natoms):
+    ...         fc[ia, :, ja, :] -= d2[(i == ia) & (j == ja), :, :].sum(axis=0)
+    >>> for ia in range(natoms):
+    ...     fc[ia, :, ia, :] -= fc[ia].sum(axis=1)
 
     """
     return primitive_neighbor_list(quantities, a.pbc,
@@ -872,10 +882,8 @@ class NewPrimitiveNeighborList:
         True
         >>> indices, offsets = nl.get_neighbors(0)
         >>> for i, offset in zip(indices, offsets):
-        ...     print(
-        ...           atoms.positions[i] + offset @ atoms.get_cell()
-        ...     )  # doctest: +ELLIPSIS
-        [3.6 ... 0. ]
+        ...     print(atoms.positions[i] + offset @ atoms.get_cell())
+        ... # doctest: +SKIP
 
         Notice that if get_neighbors(a) gives atom b as a neighbor,
         then get_neighbors(b) will not return a as a neighbor - unless
@@ -1043,10 +1051,8 @@ class PrimitiveNeighborList:
         True
         >>> indices, offsets = nl.get_neighbors(0)
         >>> for i, offset in zip(indices, offsets):
-        ...     print(
-        ...           atoms.positions[i] + offset @ atoms.get_cell()
-        ...     )  # doctest: +ELLIPSIS
-        [3.6 ... 0. ]
+        ...     print(atoms.positions[i] + offset @ atoms.get_cell())
+        ... # doctest: +SKIP
 
         Notice that if get_neighbors(a) gives atom b as a neighbor,
         then get_neighbors(b) will not return a as a neighbor - unless
diff -pruN 3.24.0-1/ase/nomad.py 3.26.0-1/ase/nomad.py
--- 3.24.0-1/ase/nomad.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/nomad.py	2025-08-12 11:26:23.000000000 +0000
@@ -6,28 +6,6 @@ import ase.units as units
 from ase import Atoms
 from ase.data import chemical_symbols
 
-nomad_api_template = ('https://labdev-nomad.esc.rzg.mpg.de/'
-                      'api/resolve/{hash}?format=recursiveJson')
-
-
-def nmd2https(uri):
-    """Get https URI corresponding to given nmd:// URI."""
-    assert uri.startswith('nmd://')
-    return nomad_api_template.format(hash=uri[6:])
-
-
-def download(uri):
-    """Download data at nmd:// URI as a NomadEntry object."""
-    try:
-        from urllib2 import urlopen
-    except ImportError:
-        from urllib.request import urlopen
-
-    httpsuri = nmd2https(uri)
-    response = urlopen(httpsuri)
-    txt = response.read().decode('utf8')
-    return json.loads(txt, object_hook=NomadEntry)
-
 
 def read(fd, _includekeys=lambda key: True):
     """Read NomadEntry object from file."""
@@ -73,13 +51,6 @@ def section_system_to_atoms(section):
     return atoms
 
 
-def nomad_entry_to_images(section):
-    """Yield the images from a Nomad entry.
-
-    The entry must contain a section_run.
-    One atoms object will be yielded for each section_system."""
-
-
 class NomadEntry(dict):
     """An entry from the Nomad database.
 
@@ -128,15 +99,3 @@ class NomadEntry(dict):
                 if self.get('name') == 'calculation_context':
                     atoms.info['nomad_calculation_uri'] = self['uri']
                 yield atoms
-
-
-def main():
-    uri = "nmd://N9Jqc1y-Bzf7sI1R9qhyyyoIosJDs/C74RJltyQeM9_WFuJYO49AR4gKuJ2"
-    print(nmd2https(uri))
-    entry = download(uri)
-    from ase.visualize import view
-    view(list(entry.iterimages()))
-
-
-if __name__ == '__main__':
-    main()
diff -pruN 3.24.0-1/ase/optimize/__init__.py 3.26.0-1/ase/optimize/__init__.py
--- 3.24.0-1/ase/optimize/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Structure optimization. """
 
 from ase.optimize.bfgs import BFGS
diff -pruN 3.24.0-1/ase/optimize/basin.py 3.26.0-1/ase/optimize/basin.py
--- 3.24.0-1/ase/optimize/basin.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/basin.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import IO, Type, Union
 
 import numpy as np
@@ -72,11 +74,11 @@ class BasinHopping(Dynamics):
         return d
 
     def initialize(self):
-        positions = self.optimizable.get_positions()
+        positions = self.optimizable.get_x().reshape(-1, 3)
         self.positions = np.zeros_like(positions)
         self.Emin = self.get_energy(positions) or 1.e32
-        self.rmin = self.optimizable.get_positions()
-        self.positions = self.optimizable.get_positions()
+        self.rmin = self.optimizable.get_x().reshape(-1, 3)
+        self.positions = self.optimizable.get_x().reshape(-1, 3)
         self.call_observers()
         self.log(-1, self.Emin, self.Emin)
 
@@ -95,7 +97,7 @@ class BasinHopping(Dynamics):
             if En < self.Emin:
                 # new minimum found
                 self.Emin = En
-                self.rmin = self.optimizable.get_positions()
+                self.rmin = self.optimizable.get_x().reshape(-1, 3)
                 self.call_observers()
             self.log(step, En, self.Emin)
 
@@ -145,7 +147,7 @@ class BasinHopping(Dynamics):
         """Return the energy of the nearest local minimum."""
         if np.any(self.positions != positions):
             self.positions = positions
-            self.optimizable.set_positions(positions)
+            self.optimizable.set_x(positions.ravel())
 
             with self.optimizer(self.optimizable,
                                 logfile=self.optimizer_logfile) as opt:
@@ -153,6 +155,6 @@ class BasinHopping(Dynamics):
             if self.lm_trajectory is not None:
                 self.lm_trajectory.write(self.optimizable)
 
-            self.energy = self.optimizable.get_potential_energy()
+            self.energy = self.optimizable.get_value()
 
         return self.energy
diff -pruN 3.24.0-1/ase/optimize/bfgs.py 3.26.0-1/ase/optimize/bfgs.py
--- 3.24.0-1/ase/optimize/bfgs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/bfgs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,7 @@
+# fmt: off
+
 import warnings
+from pathlib import Path
 from typing import IO, Optional, Union
 
 import numpy as np
@@ -16,8 +19,8 @@ class BFGS(Optimizer):
         self,
         atoms: Atoms,
         restart: Optional[str] = None,
-        logfile: Optional[Union[IO, str]] = '-',
-        trajectory: Optional[str] = None,
+        logfile: Optional[Union[IO, str, Path]] = '-',
+        trajectory: Optional[Union[str, Path]] = None,
         append_trajectory: bool = False,
         maxstep: Optional[float] = None,
         alpha: Optional[float] = None,
@@ -35,10 +38,10 @@ class BFGS(Optimizer):
             such a name will be searched and hessian matrix stored will
             be used, if the file exists.
 
-        trajectory: str
+        trajectory: str or Path
             Trajectory file used to store optimisation path.
 
-        logfile: file object or str
+        logfile: file object, Path, or str
             If *logfile* is a string, a file with that name will be opened.
             Use '-' for stdout.
 
@@ -76,7 +79,7 @@ class BFGS(Optimizer):
 
     def initialize(self):
         # initial hessian
-        self.H0 = np.eye(3 * len(self.optimizable)) * self.alpha
+        self.H0 = np.eye(self.optimizable.ndofs()) * self.alpha
 
         self.H = None
         self.pos0 = None
@@ -90,25 +93,26 @@ class BFGS(Optimizer):
         else:
             self.H, self.pos0, self.forces0, self.maxstep = file
 
-    def step(self, forces=None):
+    def step(self, gradient=None):
         optimizable = self.optimizable
 
-        if forces is None:
-            forces = optimizable.get_forces()
+        if gradient is None:
+            gradient = optimizable.get_gradient()
 
-        pos = optimizable.get_positions()
-        dpos, steplengths = self.prepare_step(pos, forces)
+        pos = optimizable.get_x()
+        dpos, steplengths = self.prepare_step(pos, gradient)
         dpos = self.determine_step(dpos, steplengths)
-        optimizable.set_positions(pos + dpos)
+        optimizable.set_x(pos + dpos)
         if isinstance(self.atoms, UnitCellFilter):
             self.dump((self.H, self.pos0, self.forces0, self.maxstep,
                        self.atoms.orig_cell))
         else:
             self.dump((self.H, self.pos0, self.forces0, self.maxstep))
 
-    def prepare_step(self, pos, forces):
-        forces = forces.reshape(-1)
-        self.update(pos.flat, forces, self.pos0, self.forces0)
+    def prepare_step(self, pos, gradient):
+        pos = pos.ravel()
+        gradient = gradient.ravel()
+        self.update(pos, gradient, self.pos0, self.forces0)
         omega, V = eigh(self.H)
 
         # FUTURE: Log this properly
@@ -123,10 +127,12 @@ class BFGS(Optimizer):
         #         self.logfile.write(msg)
         #         self.logfile.flush()
 
-        dpos = np.dot(V, np.dot(forces, V) / np.fabs(omega)).reshape((-1, 3))
-        steplengths = (dpos**2).sum(1)**0.5
-        self.pos0 = pos.flat.copy()
-        self.forces0 = forces.copy()
+        dpos = np.dot(V, np.dot(gradient, V) / np.fabs(omega))
+        # XXX Here we are calling gradient_norm() on some positions.
+        # Should there be a general norm concept
+        steplengths = self.optimizable.gradient_norm(dpos)
+        self.pos0 = pos
+        self.forces0 = gradient.copy()
         return dpos, steplengths
 
     def determine_step(self, dpos, steplengths):
diff -pruN 3.24.0-1/ase/optimize/bfgslinesearch.py 3.26.0-1/ase/optimize/bfgslinesearch.py
--- 3.24.0-1/ase/optimize/bfgslinesearch.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/bfgslinesearch.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # ******NOTICE***************
 # optimize.py module by Travis E. Oliphant
 #
@@ -9,7 +11,7 @@ import time
 from typing import IO, Optional, Union
 
 import numpy as np
-from numpy import absolute, eye, isinf, sqrt
+from numpy import absolute, eye, isinf
 
 from ase import Atoms
 from ase.optimize.optimize import Optimizer
@@ -107,13 +109,9 @@ class BFGSLineSearch(Optimizer):
         optimizable = self.optimizable
 
         if forces is None:
-            forces = optimizable.get_forces()
+            forces = optimizable.get_gradient().reshape(-1, 3)
 
-        if optimizable.is_neb():
-            raise TypeError('NEB calculations cannot use the BFGSLineSearch'
-                            ' optimizer. Use BFGS or another optimizer.')
-        r = optimizable.get_positions()
-        r = r.reshape(-1)
+        r = optimizable.get_x()
         g = -forces.reshape(-1) / self.alpha
         p0 = self.p
         self.update(r, g, self.r0, self.g0, p0)
@@ -122,8 +120,8 @@ class BFGSLineSearch(Optimizer):
 
         self.p = -np.dot(self.H, g)
         p_size = np.sqrt((self.p**2).sum())
-        if p_size <= np.sqrt(len(optimizable) * 1e-10):
-            self.p /= (p_size / np.sqrt(len(optimizable) * 1e-10))
+        if p_size <= np.sqrt(optimizable.ndofs() / 3 * 1e-10):
+            self.p /= (p_size / np.sqrt(optimizable.ndofs() / 3 * 1e-10))
         ls = LineSearch()
         self.alpha_k, e, self.e0, self.no_update = \
             ls._line_search(self.func, self.fprime, r, self.p, g, e, self.e0,
@@ -133,15 +131,15 @@ class BFGSLineSearch(Optimizer):
             raise RuntimeError("LineSearch failed!")
 
         dr = self.alpha_k * self.p
-        optimizable.set_positions((r + dr).reshape(len(optimizable), -1))
+        optimizable.set_x(r + dr)
         self.r0 = r
         self.g0 = g
         self.dump((self.r0, self.g0, self.e0, self.task, self.H))
 
     def update(self, r, g, r0, g0, p0):
-        self.I = eye(len(self.optimizable) * 3, dtype=int)
+        self.I = eye(self.optimizable.ndofs(), dtype=int)
         if self.H is None:
-            self.H = eye(3 * len(self.optimizable))
+            self.H = eye(self.optimizable.ndofs())
             # self.B = np.linalg.inv(self.H)
             return
         else:
@@ -172,18 +170,18 @@ class BFGSLineSearch(Optimizer):
 
     def func(self, x):
         """Objective function for use of the optimizers"""
-        self.optimizable.set_positions(x.reshape(-1, 3))
+        self.optimizable.set_x(x)
         self.function_calls += 1
         # Scale the problem as SciPy uses I as initial Hessian.
-        return self.optimizable.get_potential_energy() / self.alpha
+        return self.optimizable.get_value() / self.alpha
 
     def fprime(self, x):
         """Gradient of the objective function for use of the optimizers"""
-        self.optimizable.set_positions(x.reshape(-1, 3))
+        self.optimizable.set_x(x)
         self.force_calls += 1
         # Remember that forces are minus the gradient!
         # Scale the problem as SciPy uses I as initial Hessian.
-        forces = self.optimizable.get_forces().reshape(-1)
+        forces = self.optimizable.get_gradient()
         return - forces / self.alpha
 
     def replay_trajectory(self, traj):
@@ -208,13 +206,11 @@ class BFGSLineSearch(Optimizer):
             self.r0 = r0
             self.g0 = g0
 
-    def log(self, forces=None):
+    def log(self, gradient):
         if self.logfile is None:
             return
-        if forces is None:
-            forces = self.optimizable.get_forces()
-        fmax = sqrt((forces**2).sum(axis=1).max())
-        e = self.optimizable.get_potential_energy()
+        fmax = self.optimizable.gradient_norm(gradient)
+        e = self.optimizable.get_value()
         T = time.localtime()
         name = self.__class__.__name__
         w = self.logfile.write
diff -pruN 3.24.0-1/ase/optimize/cellawarebfgs.py 3.26.0-1/ase/optimize/cellawarebfgs.py
--- 3.24.0-1/ase/optimize/cellawarebfgs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/cellawarebfgs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import time
 from typing import IO, Optional, Union
 
@@ -91,9 +93,9 @@ class CellAwareBFGS(BFGS):
         cell_H[np.ix_(ind, ind)] = C_ijkl.reshape((9, 9))[
             np.ix_(ind, ind)] * self.atoms.atoms.cell.volume
 
-    def converged(self, forces=None):
-        if forces is None:
-            forces = self.atoms.atoms.get_forces()
+    def converged(self, gradient):
+        # XXX currently ignoring gradient
+        forces = self.atoms.atoms.get_forces()
         stress = self.atoms.atoms.get_stress(voigt=False) * self.atoms.mask
         return np.max(np.sum(forces**2, axis=1))**0.5 < self.fmax and \
             np.max(np.abs(stress)) < self.smax
@@ -103,14 +105,14 @@ class CellAwareBFGS(BFGS):
         self.fmax = fmax
         self.smax = smax
         if steps is not None:
-            self.max_steps = steps
+            return Dynamics.run(self, steps=steps)
         return Dynamics.run(self)
 
-    def log(self, forces=None):
-        if forces is None:
-            forces = self.atoms.atoms.get_forces()
+    def log(self, gradient):
+        # XXX ignoring gradient
+        forces = self.atoms.atoms.get_forces()
         fmax = (forces ** 2).sum(axis=1).max() ** 0.5
-        e = self.optimizable.get_potential_energy()
+        e = self.optimizable.get_value()
         T = time.localtime()
         smax = abs(self.atoms.atoms.get_stress(voigt=False) *
                    self.atoms.mask).max()
diff -pruN 3.24.0-1/ase/optimize/climbfixinternals.py 3.26.0-1/ase/optimize/climbfixinternals.py
--- 3.24.0-1/ase/optimize/climbfixinternals.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/climbfixinternals.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import IO, Any, Dict, List, Optional, Type, Union
 
 from numpy.linalg import norm
@@ -120,6 +122,9 @@ class BFGSClimbFixInternals(BFGS):
         self.autolog = 'logfile' not in self.optB_kwargs
         self.autotraj = 'trajectory' not in self.optB_kwargs
 
+    def Nx3(self, array):
+        return array.reshape(-1, 3)
+
     def read(self):
         (self.H, self.pos0, self.forces0, self.maxstep,
          self.targetvalue) = self.load()
@@ -136,18 +141,19 @@ class BFGSClimbFixInternals(BFGS):
     def pretend2climb(self):
         """Get directions for climbing and climb with optimizer 'A'."""
         proj_forces = self.get_projected_forces()
-        pos = self.optimizable.get_positions()
+        pos = self.optimizable.get_x()
         dpos, steplengths = self.prepare_step(pos, proj_forces)
         dpos = self.determine_step(dpos, steplengths)
         return pos, dpos
 
     def update_positions_and_targetvalue(self, pos, dpos):
         """Adjust constrained targetvalue of constraint and update positions."""
-        self.constr2climb.adjust_positions(pos, pos + dpos)  # update sigma
+        self.constr2climb.adjust_positions(
+            self.Nx3(pos), self.Nx3(pos + dpos))  # update sigma
         self.targetvalue += self.constr2climb.sigma          # climb constraint
         self.constr2climb.targetvalue = self.targetvalue     # adjust positions
-        self.optimizable.set_positions(
-            self.optimizable.get_positions())   # to targetvalue
+        # XXX very magical ...
+        self.optimizable.set_x(self.optimizable.get_x())   # to targetvalue
 
     def relax_remaining_dof(self):
         """Optimize remaining degrees of freedom with optimizer 'B'."""
@@ -158,7 +164,8 @@ class BFGSClimbFixInternals(BFGS):
         fmax = self.get_scaled_fmax()
         with self.optB(self.optimizable.atoms, **self.optB_kwargs) as opt:
             opt.run(fmax)  # optimize with scaled fmax
-            if self.converged() and fmax > self.optB_fmax:
+            grad = self.optimizable.get_gradient()
+            if self.converged(grad) and fmax > self.optB_fmax:
                 # (final) optimization with desired fmax
                 opt.run(self.optB_fmax)
 
@@ -172,21 +179,24 @@ class BFGSClimbFixInternals(BFGS):
         """Return the projected forces along the constrained coordinate in
         uphill direction (negative sign)."""
         forces = self.constr2climb.projected_forces
-        forces = -forces.reshape(self.optimizable.get_positions().shape)
+        # XXX simplify me once optimizable shape shenanigans have converged
+        forces = -forces.ravel()
         return forces
 
     def get_total_forces(self):
         """Return forces obeying all constraints plus projected forces."""
-        return self.optimizable.get_forces() + self.get_projected_forces()
+        forces = self.optimizable.get_gradient()
+        return forces + self.get_projected_forces()
 
-    def converged(self, forces=None):
+    def converged(self, gradient):
         """Did the optimization converge based on the total forces?"""
-        forces = forces or self.get_total_forces()
-        return super().converged(forces=forces)
-
-    def log(self, forces=None):
-        forces = forces or self.get_total_forces()
-        super().log(forces=forces)
+        # XXX ignoring gradient
+        gradient = self.get_total_forces().ravel()
+        return super().converged(gradient=gradient)
+
+    def log(self, gradient):
+        forces = self.get_total_forces()
+        super().log(gradient=forces.ravel())
 
 
 def get_fixinternals(atoms):
diff -pruN 3.24.0-1/ase/optimize/fire.py 3.26.0-1/ase/optimize/fire.py
--- 3.24.0-1/ase/optimize/fire.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/fire.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import IO, Any, Callable, Dict, List, Optional, Union
 
 import numpy as np
@@ -163,18 +165,18 @@ class FIRE(Optimizer):
         optimizable = self.optimizable
 
         if f is None:
-            f = optimizable.get_forces()
+            f = optimizable.get_gradient().reshape(-1, 3)
 
         if self.v is None:
-            self.v = np.zeros((len(optimizable), 3))
+            self.v = np.zeros(optimizable.ndofs()).reshape(-1, 3)
             if self.downhill_check:
-                self.e_last = optimizable.get_potential_energy()
-                self.r_last = optimizable.get_positions().copy()
+                self.e_last = optimizable.get_value()
+                self.r_last = optimizable.get_x().reshape(-1, 3).copy()
                 self.v_last = self.v.copy()
         else:
             is_uphill = False
             if self.downhill_check:
-                e = optimizable.get_potential_energy()
+                e = optimizable.get_value()
                 # Check if the energy actually decreased
                 if e > self.e_last:
                     # If not, reset to old positions...
@@ -182,10 +184,10 @@ class FIRE(Optimizer):
                         self.position_reset_callback(
                             optimizable, self.r_last, e,
                             self.e_last)
-                    optimizable.set_positions(self.r_last)
+                    optimizable.set_x(self.r_last.ravel())
                     is_uphill = True
-                self.e_last = optimizable.get_potential_energy()
-                self.r_last = optimizable.get_positions().copy()
+                self.e_last = optimizable.get_value()
+                self.r_last = optimizable.get_x().reshape(-1, 3).copy()
                 self.v_last = self.v.copy()
 
             vf = np.vdot(f, self.v)
@@ -207,6 +209,6 @@ class FIRE(Optimizer):
         normdr = np.sqrt(np.vdot(dr, dr))
         if normdr > self.maxstep:
             dr = self.maxstep * dr / normdr
-        r = optimizable.get_positions()
-        optimizable.set_positions(r + dr)
+        r = optimizable.get_x().reshape(-1, 3)
+        optimizable.set_x((r + dr).ravel())
         self.dump((self.v, self.dt))
diff -pruN 3.24.0-1/ase/optimize/fire2.py 3.26.0-1/ase/optimize/fire2.py
--- 3.24.0-1/ase/optimize/fire2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/fire2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # ######################################
 # Implementation of FIRE2.0 and ABC-FIRE
 
@@ -146,10 +148,10 @@ class FIRE2(Optimizer):
         optimizable = self.optimizable
 
         if f is None:
-            f = optimizable.get_forces()
+            f = optimizable.get_gradient().reshape(-1, 3)
 
         if self.v is None:
-            self.v = np.zeros((len(optimizable), 3))
+            self.v = np.zeros(optimizable.ndofs()).reshape(-1, 3)
         else:
 
             vf = np.vdot(f, self.v)
@@ -165,12 +167,12 @@ class FIRE2(Optimizer):
                 self.a = self.astart
 
                 dr = - 0.5 * self.dt * self.v
-                r = optimizable.get_positions()
-                optimizable.set_positions(r + dr)
+                r = optimizable.get_x().reshape(-1, 3)
+                optimizable.set_x((r + dr).ravel())
                 self.v[:] *= 0.0
 
         # euler semi implicit
-        f = optimizable.get_forces()
+        f = optimizable.get_gradient().reshape(-1, 3)
         self.v += self.dt * f
 
         if self.use_abc:
@@ -208,7 +210,7 @@ class FIRE2(Optimizer):
             if normdr > self.maxstep:
                 dr = self.maxstep * dr / normdr
 
-        r = optimizable.get_positions()
-        optimizable.set_positions(r + dr)
+        r = optimizable.get_x().reshape(-1, 3)
+        optimizable.set_x((r + dr).ravel())
 
         self.dump((self.v, self.dt))
diff -pruN 3.24.0-1/ase/optimize/gpmin/gp.py 3.26.0-1/ase/optimize/gpmin/gp.py
--- 3.24.0-1/ase/optimize/gpmin/gp.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/gpmin/gp.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 from scipy.linalg import cho_factor, cho_solve, solve_triangular
 from scipy.optimize import minimize
diff -pruN 3.24.0-1/ase/optimize/gpmin/gpmin.py 3.26.0-1/ase/optimize/gpmin/gpmin.py
--- 3.24.0-1/ase/optimize/gpmin/gpmin.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/gpmin/gpmin.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import warnings
 
 import numpy as np
@@ -208,7 +210,6 @@ class GPMin(Optimizer, GaussianProcess):
         """
         # update the training set
         self.x_list.append(r)
-        f = f.reshape(-1)
         y = np.append(np.array(e).reshape(-1), -f)
         self.y_list.append(y)
 
@@ -251,16 +252,16 @@ class GPMin(Optimizer, GaussianProcess):
     def step(self, f=None):
         optimizable = self.optimizable
         if f is None:
-            f = optimizable.get_forces()
+            f = optimizable.get_gradient().reshape(-1, 3)
 
-        r0 = optimizable.get_positions().reshape(-1)
-        e0 = optimizable.get_potential_energy()
+        r0 = optimizable.get_x()
+        e0 = optimizable.get_value()
         self.update(r0, e0, f)
 
         r1 = self.relax_model(r0)
-        optimizable.set_positions(r1.reshape(-1, 3))
-        e1 = optimizable.get_potential_energy()
-        f1 = optimizable.get_forces()
+        optimizable.set_x(r1)
+        e1 = optimizable.get_value()
+        f1 = optimizable.get_gradient()
         self.function_calls += 1
         self.force_calls += 1
         count = 0
@@ -268,9 +269,9 @@ class GPMin(Optimizer, GaussianProcess):
             self.update(r1, e1, f1)
             r1 = self.relax_model(r0)
 
-            optimizable.set_positions(r1.reshape(-1, 3))
-            e1 = optimizable.get_potential_energy()
-            f1 = optimizable.get_forces()
+            optimizable.set_x(r1)
+            e1 = optimizable.get_value()
+            f1 = optimizable.get_gradient()
             self.function_calls += 1
             self.force_calls += 1
             if self.converged(f1):
diff -pruN 3.24.0-1/ase/optimize/gpmin/kernel.py 3.26.0-1/ase/optimize/gpmin/kernel.py
--- 3.24.0-1/ase/optimize/gpmin/kernel.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/gpmin/kernel.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 import numpy.linalg as la
 
diff -pruN 3.24.0-1/ase/optimize/gpmin/prior.py 3.26.0-1/ase/optimize/gpmin/prior.py
--- 3.24.0-1/ase/optimize/gpmin/prior.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/gpmin/prior.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 
diff -pruN 3.24.0-1/ase/optimize/lbfgs.py 3.26.0-1/ase/optimize/lbfgs.py
--- 3.24.0-1/ase/optimize/lbfgs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/lbfgs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import IO, Optional, Union
 
 import numpy as np
@@ -122,9 +124,9 @@ class LBFGS(Optimizer):
         then take it"""
 
         if forces is None:
-            forces = self.optimizable.get_forces()
+            forces = self.optimizable.get_gradient().reshape(-1, 3)
 
-        pos = self.optimizable.get_positions()
+        pos = self.optimizable.get_x().reshape(-1, 3)
 
         self.update(pos, forces, self.r0, self.f0)
 
@@ -154,12 +156,12 @@ class LBFGS(Optimizer):
         if self.use_line_search is True:
             e = self.func(pos)
             self.line_search(pos, g, e)
-            dr = (self.alpha_k * self.p).reshape(len(self.optimizable), -1)
+            dr = (self.alpha_k * self.p).reshape(-1, 3)
         else:
             self.force_calls += 1
             self.function_calls += 1
             dr = self.determine_step(self.p) * self.damping
-        self.optimizable.set_positions(pos + dr)
+        self.optimizable.set_x((pos + dr).ravel())
 
         self.iteration += 1
         self.r0 = pos
@@ -222,22 +224,22 @@ class LBFGS(Optimizer):
 
     def func(self, x):
         """Objective function for use of the optimizers"""
-        self.optimizable.set_positions(x.reshape(-1, 3))
+        self.optimizable.set_x(x)
         self.function_calls += 1
-        return self.optimizable.get_potential_energy()
+        return self.optimizable.get_value()
 
     def fprime(self, x):
         """Gradient of the objective function for use of the optimizers"""
-        self.optimizable.set_positions(x.reshape(-1, 3))
+        self.optimizable.set_x(x)
         self.force_calls += 1
         # Remember that forces are minus the gradient!
-        return - self.optimizable.get_forces().reshape(-1)
+        return -self.optimizable.get_gradient()
 
     def line_search(self, r, g, e):
         self.p = self.p.ravel()
         p_size = np.sqrt((self.p**2).sum())
-        if p_size <= np.sqrt(len(self.optimizable) * 1e-10):
-            self.p /= (p_size / np.sqrt(len(self.optimizable) * 1e-10))
+        if p_size <= np.sqrt(self.optimizable.ndofs() / 3 * 1e-10):
+            self.p /= (p_size / np.sqrt(self.optimizable.ndofs() / 3 * 1e-10))
         g = g.ravel()
         r = r.ravel()
         ls = LineSearch()
diff -pruN 3.24.0-1/ase/optimize/mdmin.py 3.26.0-1/ase/optimize/mdmin.py
--- 3.24.0-1/ase/optimize/mdmin.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/mdmin.py	2025-08-12 11:26:23.000000000 +0000
@@ -65,10 +65,10 @@ class MDMin(Optimizer):
         optimizable = self.optimizable
 
         if forces is None:
-            forces = optimizable.get_forces()
+            forces = optimizable.get_gradient().reshape(-1, 3)
 
         if self.v is None:
-            self.v = np.zeros((len(optimizable), 3))
+            self.v = np.zeros(optimizable.ndofs()).reshape(-1, 3)
         else:
             self.v += 0.5 * self.dt * forces
             # Correct velocities:
@@ -79,7 +79,7 @@ class MDMin(Optimizer):
                 self.v[:] = forces * vf / np.vdot(forces, forces)
 
         self.v += 0.5 * self.dt * forces
-        pos = optimizable.get_positions()
+        pos = optimizable.get_x().reshape(-1, 3)
         dpos = self.dt * self.v
 
         # For any dpos magnitude larger than maxstep, scaling
@@ -88,5 +88,5 @@ class MDMin(Optimizer):
         # than self.maxstep are scaled to it.
         scaling = self.maxstep / (1e-6 + np.max(np.linalg.norm(dpos, axis=1)))
         dpos *= np.clip(scaling, 0.0, 1.0)
-        optimizable.set_positions(pos + dpos)
+        optimizable.set_x((pos + dpos).ravel())
         self.dump((self.v, self.dt))
diff -pruN 3.24.0-1/ase/optimize/minimahopping.py 3.26.0-1/ase/optimize/minimahopping.py
--- 3.24.0-1/ase/optimize/minimahopping.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/minimahopping.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 
 import numpy as np
diff -pruN 3.24.0-1/ase/optimize/ode.py 3.26.0-1/ase/optimize/ode.py
--- 3.24.0-1/ase/optimize/ode.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/ode.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import IO, Optional, Union
 
 import numpy as np
@@ -212,5 +214,5 @@ class ODE12r(SciPyOptimizer):
                verbose=self.verbose,
                apply_precon=self.apply_precon,
                callback=self.callback,
-               converged=lambda F, X: self.converged(F.reshape(-1, 3)),
+               converged=lambda gradient, X: self.converged(gradient),
                rtol=self.rtol)
diff -pruN 3.24.0-1/ase/optimize/oldqn.py 3.26.0-1/ase/optimize/oldqn.py
--- 3.24.0-1/ase/optimize/oldqn.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/oldqn.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Copyright (C) 2003  CAMP
 # Please see the accompanying LICENSE file for further information.
 
@@ -19,7 +21,9 @@ from ase.optimize.optimize import Optimi
 
 def f(lamda, Gbar, b, radius):
     b1 = b - lamda
-    g = radius**2 - np.dot(Gbar / b1, Gbar / b1)
+    b1[abs(b1) < 1e-40] = 1e-40  # avoid divide-by-zero
+    gbar_b_lamda = Gbar / b1  # only compute once
+    g = radius**2 - np.dot(gbar_b_lamda, gbar_b_lamda)
     return g
 
 
@@ -74,8 +78,15 @@ def scale_radius_force(f, r):
 def find_lamda(upperlimit, Gbar, b, radius):
     lowerlimit = upperlimit
     step = 0.1
+    i = 0
+
     while f(lowerlimit, Gbar, b, radius) < 0:
         lowerlimit -= step
+        i += 1
+
+        # if many iterations are required, dynamically scale step for efficiency
+        if i % 100 == 0:
+            step *= 1.25
 
     converged = False
 
@@ -155,7 +166,7 @@ class GoodOldQuasiNewton(Optimizer):
         self.verbosity = verbosity
         self.diagonal = diagonal
 
-        n = len(self.optimizable) * 3
+        n = self.optimizable.ndofs()
         if radius is None:
             self.radius = 0.05 * np.sqrt(n) / 10.0
         else:
@@ -170,10 +181,6 @@ class GoodOldQuasiNewton(Optimizer):
         self.radius = max(min(self.radius, self.maxradius), 0.0001)
 
         self.transitionstate = transitionstate
-
-        if self.optimizable.is_neb():
-            self.forcemin = False
-
         self.t0 = time.time()
 
     def initialize(self):
@@ -194,7 +201,7 @@ class GoodOldQuasiNewton(Optimizer):
 
     def set_default_hessian(self):
         # set unit matrix
-        n = len(self.optimizable) * 3
+        n = self.optimizable.ndofs()
         hessian = np.zeros((n, n))
         for i in range(n):
             hessian[i][i] = self.diagonal
@@ -285,12 +292,12 @@ class GoodOldQuasiNewton(Optimizer):
         """
 
         if forces is None:
-            forces = self.optimizable.get_forces()
+            forces = self.optimizable.get_gradient().reshape(-1, 3)
 
-        pos = self.optimizable.get_positions().ravel()
-        G = -self.optimizable.get_forces().ravel()
+        pos = self.optimizable.get_x()
+        G = -self.optimizable.get_gradient()
 
-        energy = self.optimizable.get_potential_energy()
+        energy = self.optimizable.get_value()
 
         if hasattr(self, 'oldenergy'):
             self.write_log('energies ' + str(energy) +
@@ -306,7 +313,7 @@ class GoodOldQuasiNewton(Optimizer):
 
             if (energy - self.oldenergy) > de:
                 self.write_log('reject step')
-                self.optimizable.set_positions(self.oldpos.reshape((-1, 3)))
+                self.optimizable.set_x(self.oldpos)
                 G = self.oldG
                 energy = self.oldenergy
                 self.radius *= 0.5
@@ -356,7 +363,7 @@ class GoodOldQuasiNewton(Optimizer):
         for i in range(n):
             step += D[i] * V[i]
 
-        pos = self.optimizable.get_positions().ravel()
+        pos = self.optimizable.get_x()
         pos += step
 
         energy_estimate = self.get_energy_estimate(D, Gbar, b)
@@ -364,7 +371,7 @@ class GoodOldQuasiNewton(Optimizer):
         self.gbar_estimate = self.get_gbar_estimate(D, Gbar, b)
         self.old_gbar = Gbar
 
-        self.optimizable.set_positions(pos.reshape((-1, 3)))
+        self.optimizable.set_x(pos)
 
     def get_energy_estimate(self, D, Gbar, b):
 
diff -pruN 3.24.0-1/ase/optimize/optimize.py 3.26.0-1/ase/optimize/optimize.py
--- 3.24.0-1/ase/optimize/optimize.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/optimize.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,16 +1,19 @@
+# fmt: off
+
 """Structure optimization. """
 import time
 import warnings
 from collections.abc import Callable
-from math import sqrt
+from functools import cached_property
 from os.path import isfile
+from pathlib import Path
 from typing import IO, Any, Dict, List, Optional, Tuple, Union
 
 from ase import Atoms
 from ase.calculators.calculator import PropertyNotImplementedError
 from ase.filters import UnitCellFilter
 from ase.parallel import world
-from ase.utils import IOContext, lazyproperty
+from ase.utils import IOContext
 from ase.utils.abc import Optimizable
 
 DEFAULT_MAX_STEPS = 100_000_000
@@ -24,16 +27,16 @@ class OptimizableAtoms(Optimizable):
     def __init__(self, atoms):
         self.atoms = atoms
 
-    def get_positions(self):
-        return self.atoms.get_positions()
+    def get_x(self):
+        return self.atoms.get_positions().ravel()
 
-    def set_positions(self, positions):
-        self.atoms.set_positions(positions)
+    def set_x(self, x):
+        self.atoms.set_positions(x.reshape(-1, 3))
 
-    def get_forces(self):
-        return self.atoms.get_forces()
+    def get_gradient(self):
+        return self.atoms.get_forces().ravel()
 
-    @lazyproperty
+    @cached_property
     def _use_force_consistent_energy(self):
         # This boolean is in principle invalidated if the
         # calculator changes.  This can lead to weird things
@@ -51,7 +54,7 @@ class OptimizableAtoms(Optimizable):
         else:
             return True
 
-    def get_potential_energy(self):
+    def get_value(self):
         force_consistent = self._use_force_consistent_energy
         return self.atoms.get_potential_energy(
             force_consistent=force_consistent)
@@ -60,10 +63,8 @@ class OptimizableAtoms(Optimizable):
         # XXX document purpose of iterimages
         return self.atoms.iterimages()
 
-    def __len__(self):
-        # TODO: return 3 * len(self.atoms), because we want the length
-        # of this to be the number of DOFs
-        return len(self.atoms)
+    def ndofs(self):
+        return 3 * len(self.atoms)
 
 
 class Dynamics(IOContext):
@@ -72,8 +73,8 @@ class Dynamics(IOContext):
     def __init__(
         self,
         atoms: Atoms,
-        logfile: Optional[Union[IO, str]] = None,
-        trajectory: Optional[str] = None,
+        logfile: Optional[Union[IO, Path, str]] = None,
+        trajectory: Optional[Union[str, Path]] = None,
         append_trajectory: bool = False,
         master: Optional[bool] = None,
         comm=world,
@@ -87,14 +88,13 @@ class Dynamics(IOContext):
         atoms : Atoms object
             The Atoms object to operate on.
 
-        logfile : file object or str
+        logfile : file object, Path, or str
             If *logfile* is a string, a file with that name will be opened.
             Use '-' for stdout.
 
-        trajectory : Trajectory object or str
-            Attach trajectory object.  If *trajectory* is a string a
-            Trajectory will be constructed.  Use *None* for no
-            trajectory.
+        trajectory : Trajectory object, str, or Path
+            Attach a trajectory object. If *trajectory* is a string/Path, a
+            Trajectory will be constructed. Use *None* for no trajectory.
 
         append_trajectory : bool
             Defaults to False, which causes the trajectory file to be
@@ -121,7 +121,7 @@ class Dynamics(IOContext):
         self.comm = comm
 
         if trajectory is not None:
-            if isinstance(trajectory, str):
+            if isinstance(trajectory, str) or isinstance(trajectory, Path):
                 from ase.io.trajectory import Trajectory
                 mode = "a" if append_trajectory else "w"
                 trajectory = self.closelater(Trajectory(
@@ -230,11 +230,11 @@ class Dynamics(IOContext):
         self.max_steps = self.nsteps + steps
 
         # compute the initial step
-        self.optimizable.get_forces()
+        gradient = self.optimizable.get_gradient()
 
         # log the initial step
         if self.nsteps == 0:
-            self.log()
+            self.log(gradient)
 
             # we write a trajectory file if it is None
             if self.trajectory is None:
@@ -245,7 +245,8 @@ class Dynamics(IOContext):
                 self.call_observers()
 
         # check convergence
-        is_converged = self.converged()
+        gradient = self.optimizable.get_gradient()
+        is_converged = self.converged(gradient)
         yield is_converged
 
         # run the algorithm until converged or max_steps reached
@@ -255,11 +256,13 @@ class Dynamics(IOContext):
             self.nsteps += 1
 
             # log the step
-            self.log()
+            gradient = self.optimizable.get_gradient()
+            self.log(gradient)
             self.call_observers()
 
             # check convergence
-            is_converged = self.converged()
+            gradient = self.optimizable.get_gradient()
+            is_converged = self.converged(gradient)
             yield is_converged
 
     def run(self, steps=DEFAULT_MAX_STEPS):
@@ -284,12 +287,12 @@ class Dynamics(IOContext):
             pass
         return converged
 
-    def converged(self):
+    def converged(self, gradient):
         """" a dummy function as placeholder for a real criterion, e.g. in
         Optimizer """
         return False
 
-    def log(self, *args):
+    def log(self, *args, **kwargs):
         """ a dummy function as placeholder for a real logger, e.g. in
         Optimizer """
         return True
@@ -310,8 +313,8 @@ class Optimizer(Dynamics):
         self,
         atoms: Atoms,
         restart: Optional[str] = None,
-        logfile: Optional[Union[IO, str]] = None,
-        trajectory: Optional[str] = None,
+        logfile: Optional[Union[IO, str, Path]] = None,
+        trajectory: Optional[Union[str, Path]] = None,
         append_trajectory: bool = False,
         **kwargs,
     ):
@@ -325,11 +328,11 @@ class Optimizer(Dynamics):
         restart: str
             Filename for restart file. Default value is *None*.
 
-        logfile: file object or str
+        logfile: file object, Path, or str
             If *logfile* is a string, a file with that name will be opened.
             Use '-' for stdout.
 
-        trajectory: Trajectory object or str
+        trajectory: Trajectory object, Path, or str
             Attach trajectory object. If *trajectory* is a string a
             Trajectory will be constructed. Use *None* for no
             trajectory.
@@ -413,17 +416,14 @@ class Optimizer(Dynamics):
         self.fmax = fmax
         return Dynamics.run(self, steps=steps)
 
-    def converged(self, forces=None):
+    def converged(self, gradient):
         """Did the optimization converge?"""
-        if forces is None:
-            forces = self.optimizable.get_forces()
-        return self.optimizable.converged(forces, self.fmax)
-
-    def log(self, forces=None):
-        if forces is None:
-            forces = self.optimizable.get_forces()
-        fmax = sqrt((forces ** 2).sum(axis=1).max())
-        e = self.optimizable.get_potential_energy()
+        assert gradient.ndim == 1
+        return self.optimizable.converged(gradient, self.fmax)
+
+    def log(self, gradient):
+        fmax = self.optimizable.gradient_norm(gradient)
+        e = self.optimizable.get_value()
         T = time.localtime()
         if self.logfile is not None:
             name = self.__class__.__name__
diff -pruN 3.24.0-1/ase/optimize/precon/__init__.py 3.26.0-1/ase/optimize/precon/__init__.py
--- 3.24.0-1/ase/optimize/precon/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/precon/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 This module contains tools for preconditioned geometry optimisation.
 
diff -pruN 3.24.0-1/ase/optimize/precon/fire.py 3.26.0-1/ase/optimize/precon/fire.py
--- 3.24.0-1/ase/optimize/precon/fire.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/precon/fire.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import time
 
 import numpy as np
@@ -156,10 +158,10 @@ class PreconFIRE(Optimizer):
         self.smax = smax
         return Optimizer.run(self, fmax, steps)
 
-    def converged(self, forces=None):
+    def converged(self, gradient):
         """Did the optimization converge?"""
-        if forces is None:
-            forces = self._actual_atoms.get_forces()
+        # XXX ignoring gradient
+        forces = self._actual_atoms.get_forces()
         if isinstance(self._actual_atoms, UnitCellFilter):
             natoms = len(self._actual_atoms.atoms)
             forces, stress = forces[:natoms], self._actual_atoms.stress
@@ -170,9 +172,8 @@ class PreconFIRE(Optimizer):
             fmax_sq = (forces**2).sum(axis=1).max()
             return fmax_sq < self.fmax**2
 
-    def log(self, forces=None):
-        if forces is None:
-            forces = self._actual_atoms.get_forces()
+    def log(self, gradient):
+        forces = self._actual_atoms.get_forces()
         if isinstance(self._actual_atoms, UnitCellFilter):
             natoms = len(self._actual_atoms.atoms)
             forces, stress = forces[:natoms], self._actual_atoms.stress
diff -pruN 3.24.0-1/ase/optimize/precon/lbfgs.py 3.26.0-1/ase/optimize/precon/lbfgs.py
--- 3.24.0-1/ase/optimize/precon/lbfgs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/precon/lbfgs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import time
 import warnings
 from math import sqrt
@@ -367,9 +369,8 @@ class PreconLBFGS(Optimizer):
         self.smax = smax
         return Optimizer.run(self, fmax, steps)
 
-    def log(self, forces=None):
-        if forces is None:
-            forces = self._actual_atoms.get_forces()
+    def log(self, gradient):
+        forces = self._actual_atoms.get_forces()
         if isinstance(self._actual_atoms, UnitCellFilter):
             natoms = len(self._actual_atoms.atoms)
             forces, stress = forces[:natoms], self._actual_atoms.stress
@@ -396,10 +397,10 @@ class PreconLBFGS(Optimizer):
                     (name, self.nsteps, T[3], T[4], T[5], e, fmax))
             self.logfile.flush()
 
-    def converged(self, forces=None):
+    def converged(self, gradient):
         """Did the optimization converge?"""
-        if forces is None:
-            forces = self._actual_atoms.get_forces()
+        # XXX ignoring gradient
+        forces = self._actual_atoms.get_forces()
         if isinstance(self._actual_atoms, UnitCellFilter):
             natoms = len(self._actual_atoms.atoms)
             forces, stress = forces[:natoms], self._actual_atoms.stress
diff -pruN 3.24.0-1/ase/optimize/precon/neighbors.py 3.26.0-1/ase/optimize/precon/neighbors.py
--- 3.24.0-1/ase/optimize/precon/neighbors.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/precon/neighbors.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 from ase.constraints import FixAtoms
diff -pruN 3.24.0-1/ase/optimize/precon/precon.py 3.26.0-1/ase/optimize/precon/precon.py
--- 3.24.0-1/ase/optimize/precon/precon.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/precon/precon.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Implementation of the Precon abstract base class and subclasses
 """
diff -pruN 3.24.0-1/ase/optimize/sciopt.py 3.26.0-1/ase/optimize/sciopt.py
--- 3.24.0-1/ase/optimize/sciopt.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/sciopt.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import IO, Optional, Union
 
 import numpy as np
@@ -71,17 +73,17 @@ class SciPyOptimizer(Optimizer):
 
         This class is mostly usable for subclasses wanting to redefine the
         parameters (and the objective function)"""
-        return self.optimizable.get_positions().reshape(-1)
+        return self.optimizable.get_x()
 
     def f(self, x):
         """Objective function for use of the optimizers"""
-        self.optimizable.set_positions(x.reshape(-1, 3))
+        self.optimizable.set_x(x)
         # Scale the problem as SciPy uses I as initial Hessian.
-        return self.optimizable.get_potential_energy() / self.H0
+        return self.optimizable.get_value() / self.H0
 
     def fprime(self, x):
         """Gradient of the objective function for use of the optimizers"""
-        self.optimizable.set_positions(x.reshape(-1, 3))
+        self.optimizable.set_x(x)
         self.force_calls += 1
 
         if self.callback_always:
@@ -89,7 +91,7 @@ class SciPyOptimizer(Optimizer):
 
         # Remember that forces are minus the gradient!
         # Scale the problem as SciPy uses I as initial Hessian.
-        return - self.optimizable.get_forces().reshape(-1) / self.H0
+        return -self.optimizable.get_gradient() / self.H0
 
     def callback(self, x):
         """Callback function to be run after each iteration by SciPy
@@ -104,10 +106,11 @@ class SciPyOptimizer(Optimizer):
         """
         if self.nsteps < self.max_steps:
             self.nsteps += 1
-        f = self.optimizable.get_forces()
+        gradient = self.optimizable.get_gradient()
+        f = gradient.reshape(-1, 3)
         self.log(f)
         self.call_observers()
-        if self.converged(f):
+        if self.converged(gradient):
             raise Converged
 
     def run(self, fmax=0.05, steps=100000000):
@@ -116,7 +119,8 @@ class SciPyOptimizer(Optimizer):
         try:
             # As SciPy does not log the zeroth iteration, we do that manually
             if self.nsteps == 0:
-                self.log()
+                gradient = self.optimizable.get_gradient()
+                self.log(gradient)
                 self.call_observers()
 
             self.max_steps = steps + self.nsteps
@@ -125,7 +129,8 @@ class SciPyOptimizer(Optimizer):
             self.call_fmin(fmax / self.H0, steps)
         except Converged:
             pass
-        return self.converged()
+        gradient = self.optimizable.get_gradient()
+        return self.converged(gradient)
 
     def dump(self, data):
         pass
@@ -242,14 +247,14 @@ class SciPyGradientlessOptimizer(Optimiz
 
         This class is mostly usable for subclasses wanting to redefine the
         parameters (and the objective function)"""
-        return self.optimizable.get_positions().reshape(-1)
+        return self.optimizable.get_x().reshape(-1)
 
     def f(self, x):
         """Objective function for use of the optimizers"""
-        self.optimizable.set_positions(x.reshape(-1, 3))
+        self.optimizable.set_x(x)
         self.function_calls += 1
         # Scale the problem as SciPy uses I as initial Hessian.
-        return self.optimizable.get_potential_energy()
+        return self.optimizable.get_value()
 
     def callback(self, x):
         """Callback function to be run after each iteration by SciPy
@@ -259,7 +264,7 @@ class SciPyGradientlessOptimizer(Optimiz
         call something similar before as well.
         """
         # We can't assume that forces are available!
-        # f = self.optimizable.get_forces()
+        # f = self.optimizable.get_gradient().reshape(-1, 3)
         # self.log(f)
         self.call_observers()
         # if self.converged(f):
diff -pruN 3.24.0-1/ase/optimize/test/analyze.py 3.26.0-1/ase/optimize/test/analyze.py
--- 3.24.0-1/ase/optimize/test/analyze.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/test/analyze.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from collections import defaultdict
 
 from numpy import inf
diff -pruN 3.24.0-1/ase/optimize/test/generate_rst.py 3.26.0-1/ase/optimize/test/generate_rst.py
--- 3.24.0-1/ase/optimize/test/generate_rst.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/test/generate_rst.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import os
 import re
 
diff -pruN 3.24.0-1/ase/optimize/test/systems.py 3.26.0-1/ase/optimize/test/systems.py
--- 3.24.0-1/ase/optimize/test/systems.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/test/systems.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from math import cos, pi, sin
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/optimize/test/test.py 3.26.0-1/ase/optimize/test/test.py
--- 3.24.0-1/ase/optimize/test/test.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/optimize/test/test.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,12 +1,16 @@
+# fmt: off
+
 import argparse
 import traceback
 from math import pi
 from time import time
+from typing import Union
 
 import numpy as np
 
 import ase.db
 import ase.optimize
+from ase import Atoms
 from ase.calculators.emt import EMT
 from ase.io import Trajectory
 
@@ -30,16 +34,21 @@ def get_optimizer(name):
 class Wrapper:
     """Atoms-object wrapper that can count number of moves."""
 
-    def __init__(self, atoms, gridspacing=0.2, eggbox=0.0):
-        # types: (Atoms, float, float) -> None
+    def __init__(
+        self,
+        atoms: Atoms,
+        gridspacing: float = 0.2,
+        eggbox: float = 0.0,
+    ) -> None:
         self.t0 = time()
         self.texcl = 0.0
         self.nsteps = 0
         self.atoms = atoms
         self.ready = False
-        self.pos = None  # type: np.ndarray
+        self.pos: Union[np.ndarray, None] = None
         self.eggbox = eggbox
 
+        self.x = None
         if eggbox:
             # Find small unit cell for grid-points
             h = []
@@ -48,8 +57,6 @@ class Wrapper:
                 n = int(L / gridspacing)
                 h.append(axis / n)
             self.x = np.linalg.inv(h)
-        else:
-            self.x = None
 
     def get_potential_energy(self, force_consistent=False):
         t1 = time()
diff -pruN 3.24.0-1/ase/outputs.py 3.26.0-1/ase/outputs.py
--- 3.24.0-1/ase/outputs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/outputs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,7 @@
+"""Module for ``Property`` and ``Properties``."""
+
+from __future__ import annotations
+
 from abc import ABC, abstractmethod
 from collections.abc import Mapping
 from typing import Sequence, Union
@@ -6,21 +10,21 @@ import numpy as np
 
 
 class Properties(Mapping):
-    def __init__(self, dct):
-        self._dct = {}
+    def __init__(self, dct: dict) -> None:
+        self._dct: dict[str, Property] = {}
         for name, value in dct.items():
             self._setvalue(name, value)
 
-    def __len__(self):
+    def __len__(self) -> int:
         return len(self._dct)
 
     def __iter__(self):
         return iter(self._dct)
 
-    def __getitem__(self, name):
+    def __getitem__(self, name) -> Property:
         return self._dct[name]
 
-    def _setvalue(self, name, value):
+    def _setvalue(self, name: str, value) -> None:
         if name in self._dct:
             # Which error should we raise for already existing property?
             raise ValueError(f'{name} already set')
@@ -39,7 +43,7 @@ class Properties(Mapping):
 
         self._dct[name] = value
 
-    def shape_is_consistent(self, prop, value) -> bool:
+    def shape_is_consistent(self, prop: Property, value) -> bool:
         """Return whether shape of values is consistent with properties.
 
         For example, forces of shape (7, 3) are consistent
@@ -56,24 +60,24 @@ class Properties(Mapping):
                 return False
         return True
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         clsname = type(self).__name__
         return f'({clsname}({self._dct})'
 
 
-all_outputs = {}
+all_outputs: dict[str, Property] = {}
 
 
 class Property(ABC):
-    def __init__(self, name, dtype, shapespec):
+    def __init__(self, name: str, dtype: type, shapespec: tuple) -> None:
         self.name = name
-        assert dtype in [float, int]  # Others?
+        if dtype not in {float, int}:  # Others?
+            raise ValueError(dtype)
         self.dtype = dtype
         self.shapespec = shapespec
 
     @abstractmethod
-    def normalize_type(self, value):
-        ...
+    def normalize_type(self, value): ...
 
     def __repr__(self) -> str:
         typename = self.dtype.__name__  # Extend to other than float/int?
@@ -82,7 +86,7 @@ class Property(ABC):
 
 
 class ScalarProperty(Property):
-    def __init__(self, name, dtype):
+    def __init__(self, name: str, dtype: type) -> None:
         super().__init__(name, dtype, ())
 
     def normalize_type(self, value):
@@ -102,9 +106,9 @@ ShapeSpec = Union[str, int]
 
 
 def _defineprop(
-        name: str,
-        dtype: type = float,
-        shape: Union[ShapeSpec, Sequence[ShapeSpec]] = ()
+    name: str,
+    dtype: type = float,
+    shape: Union[ShapeSpec, Sequence[ShapeSpec]] = (),
 ) -> Property:
     """Create, register, and return a property."""
 
diff -pruN 3.24.0-1/ase/parallel.py 3.26.0-1/ase/parallel.py
--- 3.24.0-1/ase/parallel.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/parallel.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import atexit
 import functools
 import os
@@ -9,21 +11,6 @@ import warnings
 import numpy as np
 
 
-def get_txt(txt, rank):
-    if hasattr(txt, 'write'):
-        # Note: User-supplied object might write to files from many ranks.
-        return txt
-    elif rank == 0:
-        if txt is None:
-            return open(os.devnull, 'w')
-        elif txt == '-':
-            return sys.stdout
-        else:
-            return open(txt, 'w', 1)
-    else:
-        return open(os.devnull, 'w')
-
-
 def paropen(name, mode='r', buffering=-1, encoding=None, comm=None):
     """MPI-safe version of open function.
 
@@ -38,9 +25,11 @@ def paropen(name, mode='r', buffering=-1
     return open(name, mode, buffering, encoding)
 
 
-def parprint(*args, **kwargs):
+def parprint(*args, comm=None, **kwargs):
     """MPI-safe print - prints only from master. """
-    if world.rank == 0:
+    if comm is None:
+        comm = world
+    if comm.rank == 0:
         print(*args, **kwargs)
 
 
@@ -85,6 +74,7 @@ class MPI:
 
     * MPI4Py
     * GPAW
+    * Asap
     * a dummy implementation for serial runs
 
     """
diff -pruN 3.24.0-1/ase/phasediagram.py 3.26.0-1/ase/phasediagram.py
--- 3.24.0-1/ase/phasediagram.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/phasediagram.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import fractions
 import functools
 import re
diff -pruN 3.24.0-1/ase/phonons.py 3.26.0-1/ase/phonons.py
--- 3.24.0-1/ase/phonons.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/phonons.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Module for calculating phonons of periodic systems."""
 
 import warnings
@@ -796,8 +798,11 @@ class Phonons(Displacement):
         kpts_kc = monkhorst_pack(kpts)
         if indices is None:
             # Return the total DOS
-            omega_w = self.band_structure(kpts_kc, verbose=verbose).ravel()
-            dos = RawDOSData(omega_w, np.ones_like(omega_w))
+            omega_w = self.band_structure(kpts_kc, verbose=verbose)
+            assert omega_w.ndim == 2
+            n_kpt = omega_w.shape[0]
+            omega_w = omega_w.ravel()
+            dos = RawDOSData(omega_w, np.ones_like(omega_w) / n_kpt)
         else:
             # Return a partial DOS
             omegas, amplitudes = self.band_structure(kpts_kc,
@@ -809,7 +814,7 @@ class Phonons(Displacement):
             assert ampl_sq.ndim == 3
             assert ampl_sq.shape == omegas.shape + (len(self.indices),)
             weights = ampl_sq[:, :, indices].sum(axis=2) / ampl_sq.sum(axis=2)
-            dos = RawDOSData(omegas.ravel(), weights.ravel())
+            dos = RawDOSData(omegas.ravel(), weights.ravel() / omegas.shape[0])
         return dos
 
     @deprecated('Please use Phonons.get_dos() instead of Phonons.dos().')
diff -pruN 3.24.0-1/ase/pourbaix.py 3.26.0-1/ase/pourbaix.py
--- 3.24.0-1/ase/pourbaix.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/pourbaix.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import functools
 import re
 from collections import Counter
diff -pruN 3.24.0-1/ase/quaternions.py 3.26.0-1/ase/quaternions.py
--- 3.24.0-1/ase/quaternions.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/quaternions.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,32 +1,6 @@
-import numpy as np
-
-from ase.atoms import Atoms
-
-
-class Quaternions(Atoms):
-
-    def __init__(self, *args, **kwargs):
-        quaternions = None
-        if 'quaternions' in kwargs:
-            quaternions = np.array(kwargs['quaternions'])
-            del kwargs['quaternions']
-        Atoms.__init__(self, *args, **kwargs)
-        if quaternions is not None:
-            self.set_array('quaternions', quaternions, shape=(4,))
-            # set default shapes
-            self.set_shapes(np.array([[3, 2, 1]] * len(self)))
+# fmt: off
 
-    def set_shapes(self, shapes):
-        self.set_array('shapes', shapes, shape=(3,))
-
-    def set_quaternions(self, quaternions):
-        self.set_array('quaternions', quaternions, quaternion=(4,))
-
-    def get_shapes(self):
-        return self.get_array('shapes')
-
-    def get_quaternions(self):
-        return self.get_array('quaternions').copy()
+import numpy as np
 
 
 class Quaternion:
diff -pruN 3.24.0-1/ase/spacegroup/crystal_data.py 3.26.0-1/ase/spacegroup/crystal_data.py
--- 3.24.0-1/ase/spacegroup/crystal_data.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/spacegroup/crystal_data.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.lattice import bravais_classes
 
 _crystal_family = ('Øaammmmmmmmmmmmmoooooooooooooooooooooooooooooooooooooooooo'
diff -pruN 3.24.0-1/ase/spacegroup/spacegroup.py 3.26.0-1/ase/spacegroup/spacegroup.py
--- 3.24.0-1/ase/spacegroup/spacegroup.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/spacegroup/spacegroup.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Copyright (C) 2010, Jesper Friis
 # (see accompanying license files for details).
 """Definition of the Spacegroup class.
diff -pruN 3.24.0-1/ase/spacegroup/symmetrize.py 3.26.0-1/ase/spacegroup/symmetrize.py
--- 3.24.0-1/ase/spacegroup/symmetrize.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/spacegroup/symmetrize.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Provides utility functions for FixSymmetry class
 """
diff -pruN 3.24.0-1/ase/spacegroup/utils.py 3.26.0-1/ase/spacegroup/utils.py
--- 3.24.0-1/ase/spacegroup/utils.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/spacegroup/utils.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from typing import List
 
 import numpy as np
diff -pruN 3.24.0-1/ase/spacegroup/xtal.py 3.26.0-1/ase/spacegroup/xtal.py
--- 3.24.0-1/ase/spacegroup/xtal.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/spacegroup/xtal.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Copyright (C) 2010, Jesper Friis
 # (see accompanying license files for details).
 
diff -pruN 3.24.0-1/ase/spectrum/band_structure.py 3.26.0-1/ase/spectrum/band_structure.py
--- 3.24.0-1/ase/spectrum/band_structure.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/spectrum/band_structure.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 import ase  # Annotations
@@ -160,7 +162,7 @@ class BandStructurePlot:
         self.ax = None
         self.xcoords = None
 
-    def plot(self, ax=None, emin=-10, emax=5, filename=None,
+    def plot(self, ax=None, *, spin=None, emin=-10, emax=5, filename=None,
              show=False, ylabel=None, colors=None, point_colors=None,
              label=None, loc=None,
              cmap=None, cmin=-1.0, cmax=1.0, sortcolors=False,
@@ -170,6 +172,10 @@ class BandStructurePlot:
 
         ax: Axes
             MatPlotLib Axes object.  Will be created if not supplied.
+        spin: int or None
+            If given, only plot the specified spin channel.
+            If None, plot all spins.
+            Default: None, i.e., plot all spins.
         emin, emax: float
             Minimum and maximum energy above reference.
         filename: str
@@ -228,7 +234,14 @@ class BandStructurePlot:
         if self.ax is None:
             ax = self.prepare_plot(ax, emin, emax, ylabel)
 
-        e_skn = self.bs.energies
+        if spin is None:
+            e_skn = self.bs.energies
+        elif spin not in [0, 1]:
+            raise ValueError(f"spin should be 0 or 1, not {spin}")
+        else:
+            # Select only one spin channel.
+            e_skn = self.bs.energies[spin, np.newaxis]
+
         nspins = len(e_skn)
 
         if point_colors is None:
diff -pruN 3.24.0-1/ase/spectrum/doscollection.py 3.26.0-1/ase/spectrum/doscollection.py
--- 3.24.0-1/ase/spectrum/doscollection.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/spectrum/doscollection.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import collections
 from functools import reduce, singledispatch
 from typing import (
@@ -284,7 +286,8 @@ class DOSCollection(collections.abc.Sequ
         matches = self._select_to_list(self, info_selection, negative=True)
         return type(self)(matches)
 
-    def sum_by(self, *info_keys: str) -> 'DOSCollection':
+    # Use typehint *info_keys: str from python3.11+
+    def sum_by(self, *info_keys) -> 'DOSCollection':
         """Return a DOSCollection with some data summed by common attributes
 
         For example, if ::
@@ -395,7 +398,9 @@ class GridDOSCollection(DOSCollection):
         else:
             self._energies = np.asarray(energies)
 
-        self._weights = np.empty((len(dos_list), len(self._energies)), float)
+        self._weights: np.ndarray = np.empty(
+            (len(dos_list), len(self._energies)), float,
+        )
         self._info = []
 
         for i, dos_data in enumerate(dos_list):
diff -pruN 3.24.0-1/ase/spectrum/dosdata.py 3.26.0-1/ase/spectrum/dosdata.py
--- 3.24.0-1/ase/spectrum/dosdata.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/spectrum/dosdata.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # Refactor of DOS-like data objects
 # towards replacing ase.dft.dos and ase.dft.pdos
 import warnings
diff -pruN 3.24.0-1/ase/stress.py 3.26.0-1/ase/stress.py
--- 3.24.0-1/ase/stress.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/stress.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 # The indices of the full stiffness matrix of (orthorhombic) interest
diff -pruN 3.24.0-1/ase/symbols.py 3.26.0-1/ase/symbols.py
--- 3.24.0-1/ase/symbols.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/symbols.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import collections.abc
 import numbers
 import warnings
diff -pruN 3.24.0-1/ase/test/__init__.py 3.26.0-1/ase/test/__init__.py
--- 3.24.0-1/ase/test/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.test.testsuite import CLICommand, test
 
 __all__ = ['CLICommand', 'test']
diff -pruN 3.24.0-1/ase/test/atoms/test_atom.py 3.26.0-1/ase/test/atoms/test_atom.py
--- 3.24.0-1/ase/test/atoms/test_atom.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atom.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atom, Atoms
 
 
diff -pruN 3.24.0-1/ase/test/atoms/test_atom_scaled_pos.py 3.26.0-1/ase/test/atoms/test_atom_scaled_pos.py
--- 3.24.0-1/ase/test/atoms/test_atom_scaled_pos.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atom_scaled_pos.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/atoms/test_atoms.py 3.26.0-1/ase/test/atoms/test_atoms.py
--- 3.24.0-1/ase/test/atoms/test_atoms.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atoms.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/atoms/test_atoms_angle.py 3.26.0-1/ase/test/atoms/test_atoms_angle.py
--- 3.24.0-1/ase/test/atoms/test_atoms_angle.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atoms_angle.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/atoms/test_atoms_distance.py 3.26.0-1/ase/test/atoms/test_atoms_distance.py
--- 3.24.0-1/ase/test/atoms/test_atoms_distance.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atoms_distance.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import itertools
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/atoms/test_atoms_formula.py 3.26.0-1/ase/test/atoms/test_atoms_formula.py
--- 3.24.0-1/ase/test/atoms/test_atoms_formula.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atoms_formula.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import warnings
 
 from ase.build import add_adsorbate, fcc111
diff -pruN 3.24.0-1/ase/test/atoms/test_atoms_get_duplicates.py 3.26.0-1/ase/test/atoms/test_atoms_get_duplicates.py
--- 3.24.0-1/ase/test/atoms/test_atoms_get_duplicates.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atoms_get_duplicates.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,6 @@
+# fmt: off
+import numpy as np
+
 from ase import Atoms
 from ase.geometry import get_duplicate_atoms
 
@@ -19,12 +22,22 @@ def test_atoms_get_duplicates():
     get_duplicate_atoms(at, delete=True)
     assert len(at) == 4
 
+
+def test_no_duplicate_atoms():
+    """test if it works if no duplicates are detected."""
     at = Atoms('H3', positions=[[0., 0., 0.],
                                 [1., 0., 0.],
                                 [3, 2.2, 5.2]])
 
-    # test if it works if no duplicates are detected.
     get_duplicate_atoms(at, delete=True)
     dups = get_duplicate_atoms(at)
 
     assert dups.size == 0
+
+
+def test_pbc():
+    """test if it works under PBCs."""
+    positions = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.9]]
+    atoms = Atoms('H2', positions=positions, cell=np.eye(3), pbc=True)
+    dups = get_duplicate_atoms(atoms, cutoff=0.2)
+    np.testing.assert_array_equal(dups, [[0, 1]])
diff -pruN 3.24.0-1/ase/test/atoms/test_atoms_get_positions.py 3.26.0-1/ase/test/atoms/test_atoms_get_positions.py
--- 3.24.0-1/ase/test/atoms/test_atoms_get_positions.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atoms_get_positions.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/atoms/test_atoms_getitem.py 3.26.0-1/ase/test/atoms/test_atoms_getitem.py
--- 3.24.0-1/ase/test/atoms/test_atoms_getitem.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atoms_getitem.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.atoms import Atoms
diff -pruN 3.24.0-1/ase/test/atoms/test_atoms_indices.py 3.26.0-1/ase/test/atoms/test_atoms_indices.py
--- 3.24.0-1/ase/test/atoms/test_atoms_indices.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atoms_indices.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/atoms/test_atoms_info_copy.py 3.26.0-1/ase/test/atoms/test_atoms_info_copy.py
--- 3.24.0-1/ase/test/atoms/test_atoms_info_copy.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atoms_info_copy.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 
 
diff -pruN 3.24.0-1/ase/test/atoms/test_atoms_instantiation.py 3.26.0-1/ase/test/atoms/test_atoms_instantiation.py
--- 3.24.0-1/ase/test/atoms/test_atoms_instantiation.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_atoms_instantiation.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atom, Atoms
 
 """The documentation says:
diff -pruN 3.24.0-1/ase/test/atoms/test_build.py 3.26.0-1/ase/test/atoms/test_build.py
--- 3.24.0-1/ase/test/atoms/test_build.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_build.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atom, Atoms
diff -pruN 3.24.0-1/ase/test/atoms/test_center.py 3.26.0-1/ase/test/atoms/test_center.py
--- 3.24.0-1/ase/test/atoms/test_center.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_center.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import cos, pi, sqrt
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/atoms/test_center_nonperiodic.py 3.26.0-1/ase/test/atoms/test_center_nonperiodic.py
--- 3.24.0-1/ase/test/atoms/test_center_nonperiodic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_center_nonperiodic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/atoms/test_compare_atoms.py 3.26.0-1/ase/test/atoms/test_compare_atoms.py
--- 3.24.0-1/ase/test/atoms/test_compare_atoms.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_compare_atoms.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/atoms/test_deprecated_get_set.py 3.26.0-1/ase/test/atoms/test_deprecated_get_set.py
--- 3.24.0-1/ase/test/atoms/test_deprecated_get_set.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_deprecated_get_set.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/atoms/test_get_angles.py 3.26.0-1/ase/test/atoms/test_get_angles.py
--- 3.24.0-1/ase/test/atoms/test_get_angles.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_get_angles.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import graphene_nanoribbon
diff -pruN 3.24.0-1/ase/test/atoms/test_h2.py 3.26.0-1/ase/test/atoms/test_h2.py
--- 3.24.0-1/ase/test/atoms/test_h2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_h2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.emt import EMT
 from ase.calculators.fd import calculate_numerical_forces
diff -pruN 3.24.0-1/ase/test/atoms/test_mic.py 3.26.0-1/ase/test/atoms/test_mic.py
--- 3.24.0-1/ase/test/atoms/test_mic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_mic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 import ase
diff -pruN 3.24.0-1/ase/test/atoms/test_momenta_velocities.py 3.26.0-1/ase/test/atoms/test_momenta_velocities.py
--- 3.24.0-1/ase/test/atoms/test_momenta_velocities.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_momenta_velocities.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/atoms/test_permute_axes.py 3.26.0-1/ase/test/atoms/test_permute_axes.py
--- 3.24.0-1/ase/test/atoms/test_permute_axes.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_permute_axes.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 from numpy.testing import assert_allclose
 
diff -pruN 3.24.0-1/ase/test/atoms/test_rotate.py 3.26.0-1/ase/test/atoms/test_rotate.py
--- 3.24.0-1/ase/test/atoms/test_rotate.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_rotate.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import sqrt
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/atoms/test_rotate_euler.py 3.26.0-1/ase/test/atoms/test_rotate_euler.py
--- 3.24.0-1/ase/test/atoms/test_rotate_euler.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_rotate_euler.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import sqrt
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/atoms/test_scaled_positions.py 3.26.0-1/ase/test/atoms/test_scaled_positions.py
--- 3.24.0-1/ase/test/atoms/test_scaled_positions.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_scaled_positions.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 
 
diff -pruN 3.24.0-1/ase/test/atoms/test_set_get_angle.py 3.26.0-1/ase/test/atoms/test_set_get_angle.py
--- 3.24.0-1/ase/test/atoms/test_set_get_angle.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/atoms/test_set_get_angle.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 
 
diff -pruN 3.24.0-1/ase/test/bandstructure/test_bandpath.py 3.26.0-1/ase/test/bandstructure/test_bandpath.py
--- 3.24.0-1/ase/test/bandstructure/test_bandpath.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bandstructure/test_bandpath.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/bandstructure/test_bandstructure.py 3.26.0-1/ase/test/bandstructure/test_bandstructure.py
--- 3.24.0-1/ase/test/bandstructure/test_bandstructure.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bandstructure/test_bandstructure.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
@@ -8,24 +9,50 @@ from ase.lattice import RHL
 from ase.spectrum.band_structure import BandStructure
 
 
-def test_bandstructure(testdir, plt):
+@pytest.fixture()
+def bs_cu():
     atoms = bulk('Cu')
     path = special_paths['fcc']
     atoms.calc = FreeElectrons(nvalence=1,
-                               kpts={'path': path, 'npoints': 200})
+                              kpts={'path': path, 'npoints': 200})
     atoms.get_potential_energy()
-    bs = atoms.calc.band_structure()
-    _coords, _labelcoords, labels = bs.get_labels()
+    return atoms.calc.band_structure()
+
+
+@pytest.fixture()
+def bs_spin(bs_cu):
+    # Artificially add a second spin channel for testing
+    return BandStructure(path=bs_cu.path,
+                        energies=np.array([bs_cu.energies[0],
+                                           bs_cu.energies[0] + 1.0]),
+                        reference=bs_cu.reference)
+
+
+def test_bandstructure(bs_cu, testdir, plt):
+    _coords, _labelcoords, labels = bs_cu.get_labels()
     print(labels)
-    bs.write('hmm.json')
+    bs_cu.write('hmm.json')
     bs = BandStructure.read('hmm.json')
     _coords, _labelcoords, labels = bs.get_labels()
     print(labels)
     assert ''.join(labels) == 'GXWKGLUWLKUX'
-    bs.plot(emax=10, filename='bs.png')
-    cols = np.linspace(-1.0, 1.0, bs.energies.size)
-    cols.shape = bs.energies.shape
-    bs.plot(emax=10, point_colors=cols, filename='bs2.png')
+    bs_cu.plot(emax=10, filename='bs.png')
+    cols = np.linspace(-1.0, 1.0, bs_cu.energies.size)
+    cols.shape = bs_cu.energies.shape
+    bs_cu.plot(emax=10, point_colors=cols, filename='bs2.png')
+
+
+def test_bandstructure_with_spin(bs_spin, testdir, plt):
+    _coords, _labelcoords, labels = bs_spin.get_labels()
+    print(labels)
+    bs_spin.write('hmm_spin.json')
+    bs = BandStructure.read('hmm_spin.json')
+    _coords, _labelcoords, labels = bs.get_labels()
+    print(labels)
+    assert ''.join(labels) == 'GXWKGLUWLKUX'
+    bs_spin.plot(emax=10, filename='bs_spin.png', spin=0, linestyle='dotted')
+    bs_spin.plot(emax=10, filename='bs_spin2.png', spin=1, linestyle='solid')
+    bs_spin.plot(emax=10, filename='bs_spin_all.png', colors='rb')
 
 
 @pytest.fixture()
diff -pruN 3.24.0-1/ase/test/bandstructure/test_bandstructure_json.py 3.26.0-1/ase/test/bandstructure/test_bandstructure_json.py
--- 3.24.0-1/ase/test/bandstructure/test_bandstructure_json.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bandstructure/test_bandstructure_json.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk
 from ase.calculators.test import FreeElectrons
 from ase.io.jsonio import read_json
diff -pruN 3.24.0-1/ase/test/bandstructure/test_bandstructure_many.py 3.26.0-1/ase/test/bandstructure/test_bandstructure_many.py
--- 3.24.0-1/ase/test/bandstructure/test_bandstructure_many.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bandstructure/test_bandstructure_many.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/bandstructure/test_bandstructure_transform_mcl.py 3.26.0-1/ase/test/bandstructure/test_bandstructure_transform_mcl.py
--- 3.24.0-1/ase/test/bandstructure/test_bandstructure_transform_mcl.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bandstructure/test_bandstructure_transform_mcl.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/bandstructure/test_resolve_custom_kpoints.py 3.26.0-1/ase/test/bandstructure/test_resolve_custom_kpoints.py
--- 3.24.0-1/ase/test/bandstructure/test_resolve_custom_kpoints.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bandstructure/test_resolve_custom_kpoints.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/bravais/test_2d_cell_pbc.py 3.26.0-1/ase/test/bravais/test_2d_cell_pbc.py
--- 3.24.0-1/ase/test/bravais/test_2d_cell_pbc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bravais/test_2d_cell_pbc.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/bravais/test_bravais_type_engine.py 3.26.0-1/ase/test/bravais/test_bravais_type_engine.py
--- 3.24.0-1/ase/test/bravais/test_bravais_type_engine.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bravais/test_bravais_type_engine.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/bravais/test_eps.py 3.26.0-1/ase/test/bravais/test_eps.py
--- 3.24.0-1/ase/test/bravais/test_eps.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bravais/test_eps.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.cell import Cell
diff -pruN 3.24.0-1/ase/test/bravais/test_hex.py 3.26.0-1/ase/test/bravais/test_hex.py
--- 3.24.0-1/ase/test/bravais/test_hex.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bravais/test_hex.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/bravais/test_lattices.py 3.26.0-1/ase/test/bravais/test_lattices.py
--- 3.24.0-1/ase/test/bravais/test_lattices.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bravais/test_lattices.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/bravais/test_line_lattice.py 3.26.0-1/ase/test/bravais/test_line_lattice.py
--- 3.24.0-1/ase/test/bravais/test_line_lattice.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bravais/test_line_lattice.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.cell import Cell
 
 
diff -pruN 3.24.0-1/ase/test/bravais/test_main.py 3.26.0-1/ase/test/bravais/test_main.py
--- 3.24.0-1/ase/test/bravais/test_main.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bravais/test_main.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/bravais/test_orcc_mcl.py 3.26.0-1/ase/test/bravais/test_orcc_mcl.py
--- 3.24.0-1/ase/test/bravais/test_orcc_mcl.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bravais/test_orcc_mcl.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/bravais/test_reduce.py 3.26.0-1/ase/test/bravais/test_reduce.py
--- 3.24.0-1/ase/test/bravais/test_reduce.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bravais/test_reduce.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/bravais/test_standard_form.py 3.26.0-1/ase/test/bravais/test_standard_form.py
--- 3.24.0-1/ase/test/bravais/test_standard_form.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/bravais/test_standard_form.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Bravais lattice type check.
 
 1) For each Bravais variant, check that we recognize the
diff -pruN 3.24.0-1/ase/test/build_/__init__.py 3.26.0-1/ase/test/build_/__init__.py
--- 3.24.0-1/ase/test/build_/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/build_/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,2 +1,3 @@
+# fmt: off
 # Note: The name 'build' clashes with a built-in.
 # This would cause pytest not to find the tests.
diff -pruN 3.24.0-1/ase/test/build_/test_attach.py 3.26.0-1/ase/test/build_/test_attach.py
--- 3.24.0-1/ase/test/build_/test_attach.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/build_/test_attach.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/build_/test_build_bulk.py 3.26.0-1/ase/test/build_/test_build_bulk.py
--- 3.24.0-1/ase/test/build_/test_build_bulk.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/build_/test_build_bulk.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for `bulk`"""
 import pytest
 
diff -pruN 3.24.0-1/ase/test/build_/test_bulk.py 3.26.0-1/ase/test/build_/test_bulk.py
--- 3.24.0-1/ase/test/build_/test_bulk.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/build_/test_bulk.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/build_/test_connected.py 3.26.0-1/ase/test/build_/test_connected.py
--- 3.24.0-1/ase/test/build_/test_connected.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/build_/test_connected.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.build import molecule
 from ase.build.connected import connected_atoms, separate, split_bond
diff -pruN 3.24.0-1/ase/test/build_/test_minimize_rotation_and_translation.py 3.26.0-1/ase/test/build_/test_minimize_rotation_and_translation.py
--- 3.24.0-1/ase/test/build_/test_minimize_rotation_and_translation.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/build_/test_minimize_rotation_and_translation.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk, minimize_rotation_and_translation, molecule
diff -pruN 3.24.0-1/ase/test/build_/test_supercells.py 3.26.0-1/ase/test/build_/test_supercells.py
--- 3.24.0-1/ase/test/build_/test_supercells.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/build_/test_supercells.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import itertools
 
 import numpy as np
@@ -5,10 +6,13 @@ import pytest
 
 from ase.build import bulk
 from ase.build.supercells import (
+    all_score_funcs,
     find_optimal_cell_shape,
-    get_deviation_from_optimal_cell_shape,
     make_supercell,
 )
+from ase.geometry.cell import cell_to_cellpar
+
+sq2 = np.sqrt(2.0)
 
 
 @pytest.fixture()
@@ -101,28 +105,28 @@ def test_make_supercell_vs_repeat(prim,
     assert all(at1.symbols == at2.symbols)
 
 
+@pytest.mark.parametrize('score_func', all_score_funcs.values())
 @pytest.mark.parametrize(
     'cell, target_shape', (
         ([[1, 0, 0], [0, 1, 0], [0, 0, 1]], 'sc'),
         ([[0, 1, 1], [1, 0, 1], [1, 1, 0]], 'fcc'),
     )
 )
-def test_cell_metric_ideal(target_shape, cell):
+def test_cell_metric_ideal(target_shape, cell, score_func):
     """Test cell with the ideal shape.
 
     Test if `get_deviation_from_optimal_cell_shape` returns perfect scores
     (0.0) for the ideal cells.
-    Test also cell vectors with permutatation and elongation.
+    Test also cell vectors with permutation and elongation.
     """
+
     cell = np.asarray(cell)
     indices_permuted = itertools.permutations(range(3))
     elongations = range(1, 4)
     for perm, factor in itertools.product(indices_permuted, elongations):
-        permuted_cell = [cell[i] * factor for i in perm]
-        cell_metric = get_deviation_from_optimal_cell_shape(
-            permuted_cell,
-            target_shape=target_shape,
-        )
+        permuted_cell = np.array([cell[i] * factor for i in perm])
+        cell_metric = score_func(permuted_cell, target_shape=target_shape)
+
         assert np.isclose(cell_metric, 0.0)
 
 
@@ -135,80 +139,111 @@ def test_cell_metric_ideal(target_shape,
 def test_cell_metric_twice_larger_lattice_vector(cell, target_shape):
     """Test cell with a twice larger lattice vector than the others.
 
-    Test if `get_deviation_from_optimal_cell_shape` gives a correct value for
+    Test if score function gives a correct value for
     the cells that have a lattice vector twice longer than the others.
     """
-    # sqrt((1 - cbrt(2))**2 + (1 - cbrt(2))**2 + (2 - cbrt(2))**2) / cbrt(2)
-    cell_metric_ref = 0.6558650332
-    cell_metric = get_deviation_from_optimal_cell_shape(cell, target_shape)
-    assert np.isclose(cell_metric, cell_metric_ref)
 
+    cb2 = np.cbrt(2.0)
 
+    # cell_length
+    # (ai / a0) - 1.0
+    # sqrt((1./cb2 - 1.)**2 + (1./cb2 - 1.)**2 + (2./cb2 - 1.)**2)
+    dia1 = (1. / cb2 - 1.) ** 2
+    dia2 = (2. / cb2 - 1.) ** 2
+    ref_score_length = np.sqrt(2. * dia1 + dia2)
+
+    # cell_shape
+    # (a0 / ai) - 1.0
+    dia1 = (1. / cb2 ** 2 - 1.) ** 2
+    dia2 = (4. / cb2 ** 2 - 1.) ** 2
+    ang1 = (1. / cb2 ** 2 / 2. - 0.5) ** 2
+    ang2 = (1. / cb2 ** 2 - 0.5) ** 2
+    ref_score_shape = {}
+    ref_score_shape['sc'] = 2. * dia1 + dia2
+    ref_score_shape['fcc'] = 2. * dia1 + dia2 + 2. * ang1 + 4. * ang2
+
+    ref_scores = [ref_score_length, ref_score_shape[target_shape]]
+
+    for score_func, ref_score in zip(all_score_funcs.values(), ref_scores):
+        score = score_func(cell, target_shape)
+        assert np.isclose(score, ref_score)
+
+
+@pytest.mark.parametrize('score_func', all_score_funcs.values())
 @pytest.mark.parametrize('target_shape', ['sc', 'fcc'])
-def test_multiple_cells(target_shape: str) -> None:
+def test_multiple_cells(target_shape, score_func):
     """Test if multiple cells can be evaluated at one time."""
-    func = get_deviation_from_optimal_cell_shape
+
     cells = np.array([
         [[1, 0, 0], [0, 1, 0], [0, 0, 2]],
         [[0, 1, 1], [1, 0, 1], [2, 2, 0]],
     ])
     metrics_separate = []
     for i in range(cells.shape[0]):
-        metric = func(cells[i], target_shape)
+        metric = score_func(cells[i], target_shape)
         metrics_separate.append(metric)
-    metrics_together = func(cells, target_shape)
+    metrics_together = score_func(cells, target_shape)
     np.testing.assert_allclose(metrics_separate, metrics_together)
 
 
+@pytest.mark.parametrize('score_func', all_score_funcs.values())
 @pytest.mark.parametrize(
     'cell, target_shape', (
         ([[-1, 0, 0], [0, -1, 0], [0, 0, -1]], 'sc'),
         ([[0, -1, -1], [-1, 0, -1], [-1, -1, 0]], 'fcc'),
     )
 )
-def test_cell_metric_negative_determinant(cell, target_shape):
+def test_cell_metric_negative_determinant(cell, target_shape, score_func):
     """Test cell with negative determinant.
 
     Test if `get_deviation_from_optimal_cell_shape` works for the cells with
     negative determinants.
     """
-    cell_metric = get_deviation_from_optimal_cell_shape(cell, target_shape)
+
+    cell_metric = score_func(cell, target_shape)
     assert np.isclose(cell_metric, 0.0)
 
 
-@pytest.mark.parametrize('cell, target_shape, target_size, ref_lengths', [
-    (np.diag([1.0, 2.0, 4.0]), 'sc', 8, 4.0),
-    ([[1, 0, 0], [0, 1, 0], [0, 0, 1]], 'fcc', 2, np.sqrt(2.0)),
-    ([[0, 1, 1], [1, 0, 1], [1, 1, 0]], 'sc', 4, 2.0),
+@pytest.mark.parametrize('score_key', all_score_funcs.keys())
+@pytest.mark.parametrize('cell, target_shape, target_size, ref_cellpar', [
+    (np.diag([1.0, 2.0, 4.0]), 'sc', 8, [4.0, 4.0, 4.0, 90., 90., 90.]),
+    ([[0, 1, 1], [1, 0, 1], [1, 1, 0]], 'sc', 4, [2., 2., 2., 90., 90., 90.]),
+    (np.eye(3), 'fcc', 2, [sq2, sq2, sq2, 60.0, 60.0, 60.0])
 ])
 def test_find_optimal_cell_shape(
-        cell, target_shape, target_size, ref_lengths):
+        cell, target_shape, target_size, ref_cellpar, score_key):
     """Test `find_optimal_cell_shape`.
 
     We test from sc to sc; from sc to fcc; and from fcc to sc."""
-    supercell_matrix = find_optimal_cell_shape(cell, target_size, target_shape,
-                                               lower_limit=-1, upper_limit=1)
-    cell_metric = get_deviation_from_optimal_cell_shape(
-        supercell_matrix @ cell,
+
+    sc_matrix = find_optimal_cell_shape(cell, target_size, target_shape,
+                                        score_key=score_key,
+                                        lower_limit=-1, upper_limit=1)
+
+    score_func = all_score_funcs[score_key]
+    cell_metric = score_func(
+        sc_matrix @ cell,
         target_shape,
     )
+
+    sc = np.dot(sc_matrix, cell)
+    cellpar = cell_to_cellpar(sc)
+
     assert np.isclose(cell_metric, 0.0)
-    cell_lengths = np.linalg.norm(np.dot(supercell_matrix, cell), axis=1)
-    assert np.allclose(cell_lengths, ref_lengths)
+    assert np.allclose(cellpar, ref_cellpar)
 
 
-def test_ideal_orientation() -> None:
+@pytest.mark.parametrize('score_key', all_score_funcs.keys())
+@pytest.mark.parametrize('cell, target_shape, target_size, sc_matrix_ref', [
+    ([[1, 0, 0], [0, 1, 0], [0, 0, 1]], 'fcc', 2,
+     [[0, 1, 1], [1, 0, 1], [1, 1, 0]]),
+    ([[0, 1, 1], [1, 0, 1], [1, 1, 0]], 'sc', 4,
+     [[-1, 1, 1], [1, -1, 1], [1, 1, -1]]),
+])
+def test_ideal_orientation(cell, target_shape,
+                           target_size, sc_matrix_ref, score_key) -> None:
     """Test if the ideal orientation is selected among candidates."""
-    cell = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
-    target_size = 2
-    target_shape = 'fcc'
-    supercell_matrix_ref = [[0, 1, 1], [1, 0, 1], [1, 1, 0]]
-    supercell_matrix = find_optimal_cell_shape(cell, target_size, target_shape)
-    np.testing.assert_array_equal(supercell_matrix, supercell_matrix_ref)
-
-    cell = [[0, 1, 1], [1, 0, 1], [1, 1, 0]]
-    target_size = 4
-    target_shape = 'sc'
-    supercell_matrix_ref = [[-1, 1, 1], [1, -1, 1], [1, 1, -1]]
-    supercell_matrix = find_optimal_cell_shape(cell, target_size, target_shape)
-    np.testing.assert_array_equal(supercell_matrix, supercell_matrix_ref)
+
+    sc_matrix = find_optimal_cell_shape(cell, target_size, target_shape,
+                                        score_key=score_key)
+    np.testing.assert_array_equal(sc_matrix, sc_matrix_ref)
diff -pruN 3.24.0-1/ase/test/build_/test_surface.py 3.26.0-1/ase/test/build_/test_surface.py
--- 3.24.0-1/ase/test/build_/test_surface.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/build_/test_surface.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for `surface`"""
 import math
 
diff -pruN 3.24.0-1/ase/test/build_/test_surface_stack.py 3.26.0-1/ase/test/build_/test_surface_stack.py
--- 3.24.0-1/ase/test/build_/test_surface_stack.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/build_/test_surface_stack.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import stack
 from ase.build.surface import _all_surface_functions
 from ase.calculators.calculator import compare_atoms
diff -pruN 3.24.0-1/ase/test/build_/test_surface_terminations.py 3.26.0-1/ase/test/build_/test_surface_terminations.py
--- 3.24.0-1/ase/test/build_/test_surface_terminations.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/build_/test_surface_terminations.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import surface
 from ase.build.surfaces_with_termination import surfaces_with_termination
 from ase.spacegroup import crystal
diff -pruN 3.24.0-1/ase/test/calculator/abinit/test_abinit_cmdline.py 3.26.0-1/ase/test/calculator/abinit/test_abinit_cmdline.py
--- 3.24.0-1/ase/test/calculator/abinit/test_abinit_cmdline.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/abinit/test_abinit_cmdline.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 
diff -pruN 3.24.0-1/ase/test/calculator/abinit/test_main.py 3.26.0-1/ase/test/calculator/abinit/test_main.py
--- 3.24.0-1/ase/test/calculator/abinit/test_main.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/abinit/test_main.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/ace/test_ace.py 3.26.0-1/ase/test/calculator/ace/test_ace.py
--- 3.24.0-1/ase/test/calculator/ace/test_ace.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/ace/test_ace.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.acemolecule import ACE
 
diff -pruN 3.24.0-1/ase/test/calculator/ace/test_ace_calculator.py 3.26.0-1/ase/test/calculator/ace/test_ace_calculator.py
--- 3.24.0-1/ase/test/calculator/ace/test_ace_calculator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/ace/test_ace_calculator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.acemolecule import ACE
 
diff -pruN 3.24.0-1/ase/test/calculator/aims/test_H2O_aims.py 3.26.0-1/ase/test/calculator/aims/test_H2O_aims.py
--- 3.24.0-1/ase/test/calculator/aims/test_H2O_aims.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/aims/test_H2O_aims.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/calculator/aims/test_aims_interface.py 3.26.0-1/ase/test/calculator/aims/test_aims_interface.py
--- 3.24.0-1/ase/test/calculator/aims/test_aims_interface.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/aims/test_aims_interface.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 import tempfile
 
@@ -95,11 +96,9 @@ def test_aims_interface():
                 sc_accuracy_forces=1e-4,
                 label=tmp_dir,
                 )
-    try:
+
+    with pytest.raises(ValueError):
         calc.prepare_input_files()
-        raise AssertionError
-    except ValueError:
-        pass
 
     calc.atoms = water
     calc.prepare_input_files()
diff -pruN 3.24.0-1/ase/test/calculator/aims/test_version.py 3.26.0-1/ase/test/calculator/aims/test_version.py
--- 3.24.0-1/ase/test/calculator/aims/test_version.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/aims/test_version.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.calculators.aims import get_aims_version
 
 version_string = """\
diff -pruN 3.24.0-1/ase/test/calculator/amber/test_amber.py 3.26.0-1/ase/test/calculator/amber/test_amber.py
--- 3.24.0-1/ase/test/calculator/amber/test_amber.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/amber/test_amber.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import subprocess
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/calculator/castep/test_castep_interface.py 3.26.0-1/ase/test/calculator/castep/test_castep_interface.py
--- 3.24.0-1/ase/test/calculator/castep/test_castep_interface.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/castep/test_castep_interface.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/calculator/conftest.py 3.26.0-1/ase/test/calculator/conftest.py
--- 3.24.0-1/ase/test/calculator/conftest.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/conftest.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.calculators.genericfileio import BaseProfile, CalculatorTemplate
diff -pruN 3.24.0-1/ase/test/calculator/cp2k/test_cp2k_H2_None.py 3.26.0-1/ase/test/calculator/cp2k/test_cp2k_H2_None.py
--- 3.24.0-1/ase/test/calculator/cp2k/test_cp2k_H2_None.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/cp2k/test_cp2k_H2_None.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test suit for the CP2K ASE calulator.
 
 http://www.cp2k.org
diff -pruN 3.24.0-1/ase/test/calculator/cp2k/test_cp2k_dcd.py 3.26.0-1/ase/test/calculator/cp2k/test_cp2k_dcd.py
--- 3.24.0-1/ase/test/calculator/cp2k/test_cp2k_dcd.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/cp2k/test_cp2k_dcd.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test suit for the CP2K ASE calulator.
 
 http://www.cp2k.org
diff -pruN 3.24.0-1/ase/test/calculator/cp2k/test_cp2k_many.py 3.26.0-1/ase/test/calculator/cp2k/test_cp2k_many.py
--- 3.24.0-1/ase/test/calculator/cp2k/test_cp2k_many.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/cp2k/test_cp2k_many.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for the CP2K ASE calculator.
 
 http://www.cp2k.org
diff -pruN 3.24.0-1/ase/test/calculator/cp2k/test_cp2k_restart.py 3.26.0-1/ase/test/calculator/cp2k/test_cp2k_restart.py
--- 3.24.0-1/ase/test/calculator/cp2k/test_cp2k_restart.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/cp2k/test_cp2k_restart.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # flake8: noqa
 
 """Test the CP2K ASE calulator.
diff -pruN 3.24.0-1/ase/test/calculator/cp2k/test_cp2k_stress.py 3.26.0-1/ase/test/calculator/cp2k/test_cp2k_stress.py
--- 3.24.0-1/ase/test/calculator/cp2k/test_cp2k_stress.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/cp2k/test_cp2k_stress.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test suit for the CP2K ASE calulator.
 
 http://www.cp2k.org
diff -pruN 3.24.0-1/ase/test/calculator/crystal/test_bulk.py 3.26.0-1/ase/test/calculator/crystal/test_bulk.py
--- 3.24.0-1/ase/test/calculator/crystal/test_bulk.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/crystal/test_bulk.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.crystal import CRYSTAL
 
diff -pruN 3.24.0-1/ase/test/calculator/crystal/test_graphene.py 3.26.0-1/ase/test/calculator/crystal/test_graphene.py
--- 3.24.0-1/ase/test/calculator/crystal/test_graphene.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/crystal/test_graphene.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.crystal import CRYSTAL
 
diff -pruN 3.24.0-1/ase/test/calculator/crystal/test_molecule.py 3.26.0-1/ase/test/calculator/crystal/test_molecule.py
--- 3.24.0-1/ase/test/calculator/crystal/test_molecule.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/crystal/test_molecule.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.atoms import Atoms
 from ase.calculators.crystal import CRYSTAL
 from ase.optimize import BFGS
diff -pruN 3.24.0-1/ase/test/calculator/demon/test_h2o.py 3.26.0-1/ase/test/calculator/demon/test_h2o.py
--- 3.24.0-1/ase/test/calculator/demon/test_h2o.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/demon/test_h2o.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/calculator/demon/test_h2o_xas_xes.py 3.26.0-1/ase/test/calculator/demon/test_h2o_xas_xes.py
--- 3.24.0-1/ase/test/calculator/demon/test_h2o_xas_xes.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/demon/test_h2o_xas_xes.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 import ase.calculators.demon as demon
diff -pruN 3.24.0-1/ase/test/calculator/demonnano/test_h2o.py 3.26.0-1/ase/test/calculator/demonnano/test_h2o.py
--- 3.24.0-1/ase/test/calculator/demonnano/test_h2o.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/demonnano/test_h2o.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/calculator/dftb/test_dftb_bandstructure.py 3.26.0-1/ase/test/calculator/dftb/test_dftb_bandstructure.py
--- 3.24.0-1/ase/test/calculator/dftb/test_dftb_bandstructure.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/dftb/test_dftb_bandstructure.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/calculator/dftb/test_dftb_relax_bulk.py 3.26.0-1/ase/test/calculator/dftb/test_dftb_relax_bulk.py
--- 3.24.0-1/ase/test/calculator/dftb/test_dftb_relax_bulk.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/dftb/test_dftb_relax_bulk.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk
 from ase.calculators.calculator import FileIOCalculator
 from ase.calculators.dftb import Dftb
diff -pruN 3.24.0-1/ase/test/calculator/dftb/test_dftb_relax_dimer.py 3.26.0-1/ase/test/calculator/dftb/test_dftb_relax_dimer.py
--- 3.24.0-1/ase/test/calculator/dftb/test_dftb_relax_dimer.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/dftb/test_dftb_relax_dimer.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/calculator/dftb/test_dftb_relax_surface.py 3.26.0-1/ase/test/calculator/dftb/test_dftb_relax_surface.py
--- 3.24.0-1/ase/test/calculator/dftb/test_dftb_relax_surface.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/dftb/test_dftb_relax_surface.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import diamond100
diff -pruN 3.24.0-1/ase/test/calculator/dftb/test_dipole.py 3.26.0-1/ase/test/calculator/dftb/test_dipole.py
--- 3.24.0-1/ase/test/calculator/dftb/test_dipole.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/dftb/test_dipole.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/dftb/test_xtb_static.py 3.26.0-1/ase/test/calculator/dftb/test_xtb_static.py
--- 3.24.0-1/ase/test/calculator/dftb/test_xtb_static.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/dftb/test_xtb_static.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/calculator/dmol/test_Al_dmol.py 3.26.0-1/ase/test/calculator/dmol/test_Al_dmol.py
--- 3.24.0-1/ase/test/calculator/dmol/test_Al_dmol.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/dmol/test_Al_dmol.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk
 from ase.calculators.dmol import DMol3
 
diff -pruN 3.24.0-1/ase/test/calculator/dmol/test_water_dmol.py 3.26.0-1/ase/test/calculator/dmol/test_water_dmol.py
--- 3.24.0-1/ase/test/calculator/dmol/test_water_dmol.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/dmol/test_water_dmol.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import molecule
 from ase.calculators.dmol import DMol3
 
diff -pruN 3.24.0-1/ase/test/calculator/elk/test_elk.py 3.26.0-1/ase/test/calculator/elk/test_elk.py
--- 3.24.0-1/ase/test/calculator/elk/test_elk.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/elk/test_elk.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,9 +1,14 @@
+"""Tests for `ELK`."""
+
 import pytest
 
+from ase import Atoms
 from ase.build import bulk
+from ase.calculators.elk import ELK
 
 
 def systems():
+    """Generate `Atoms`."""
     yield bulk('Si')
     atoms = bulk('Fe')
     atoms.set_initial_magnetic_moments([1.0])
@@ -11,11 +16,13 @@ def systems():
 
 
 @pytest.mark.calculator_lite()
-@pytest.mark.parametrize('atoms', systems(),
-                         ids=lambda atoms: str(atoms.symbols))
+@pytest.mark.parametrize(
+    'atoms', systems(), ids=lambda atoms: str(atoms.symbols)
+)
 @pytest.mark.calculator('elk', tasks=0, ngridk=(3, 3, 3))
-def test_elk_bulk(factory, atoms):
-    calc = factory.calc()
+def test_elk_bulk(factory, atoms: Atoms) -> None:
+    """Test `ELK`."""
+    calc: ELK = factory.calc()
     atoms.calc = calc
     spinpol = atoms.get_initial_magnetic_moments().any()
     props = atoms.get_properties(['energy', 'forces'])
@@ -28,8 +35,12 @@ def test_elk_bulk(factory, atoms):
 
     # Since this is FileIO we tend to just load everything there is:
     expected_props = {
-        'energy', 'free_energy', 'forces', 'ibz_kpoints',
-        'eigenvalues', 'occupations'
+        'energy',
+        'free_energy',
+        'forces',
+        'ibz_kpoints',
+        'eigenvalues',
+        'occupations',
     }
 
     assert expected_props < set(props)
@@ -43,7 +54,8 @@ def test_elk_bulk(factory, atoms):
     x = slice(None)
     assert calc.get_eigenvalues(x, x) == pytest.approx(props['eigenvalues'])
     assert calc.get_occupation_numbers(x, x) == pytest.approx(
-        props['occupations'])
+        props['occupations']
+    )
     assert calc.get_spin_polarized() == spinpol
     assert calc.get_number_of_spins() == 1 + int(spinpol)
     assert calc.get_number_of_bands() == props['nbands']
diff -pruN 3.24.0-1/ase/test/calculator/espresso/test_espresso.py 3.26.0-1/ase/test/calculator/espresso/test_espresso.py
--- 3.24.0-1/ase/test/calculator/espresso/test_espresso.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/espresso/test_espresso.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk, molecule
diff -pruN 3.24.0-1/ase/test/calculator/exciting/conftest.py 3.26.0-1/ase/test/calculator/exciting/conftest.py
--- 3.24.0-1/ase/test/calculator/exciting/conftest.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/exciting/conftest.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 
diff -pruN 3.24.0-1/ase/test/calculator/exciting/test_exciting.py 3.26.0-1/ase/test/calculator/exciting/test_exciting.py
--- 3.24.0-1/ase/test/calculator/exciting/test_exciting.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/exciting/test_exciting.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test file for exciting ASE calculator."""
 
 import xml.etree.ElementTree as ET
diff -pruN 3.24.0-1/ase/test/calculator/exciting/test_runner.py 3.26.0-1/ase/test/calculator/exciting/test_runner.py
--- 3.24.0-1/ase/test/calculator/exciting/test_runner.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/exciting/test_runner.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test runner classes to run exciting simulations using subproces."""
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/gamess_us/test_basis_ecp.py 3.26.0-1/ase/test/calculator/gamess_us/test_basis_ecp.py
--- 3.24.0-1/ase/test/calculator/gamess_us/test_basis_ecp.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/gamess_us/test_basis_ecp.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/calculator/gamess_us/test_gamess_us.py 3.26.0-1/ase/test/calculator/gamess_us/test_gamess_us.py
--- 3.24.0-1/ase/test/calculator/gamess_us/test_gamess_us.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/gamess_us/test_gamess_us.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/gaussian/test_h2of.py 3.26.0-1/ase/test/calculator/gaussian/test_h2of.py
--- 3.24.0-1/ase/test/calculator/gaussian/test_h2of.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/gaussian/test_h2of.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.gaussian import Gaussian
 
diff -pruN 3.24.0-1/ase/test/calculator/gaussian/test_optimizer_irc.py 3.26.0-1/ase/test/calculator/gaussian/test_optimizer_irc.py
--- 3.24.0-1/ase/test/calculator/gaussian/test_optimizer_irc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/gaussian/test_optimizer_irc.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/calculator/gaussian/test_water.py 3.26.0-1/ase/test/calculator/gaussian/test_water.py
--- 3.24.0-1/ase/test/calculator/gaussian/test_water.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/gaussian/test_water.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.atoms import Atoms
 from ase.calculators.gaussian import Gaussian
 from ase.io import read
diff -pruN 3.24.0-1/ase/test/calculator/gpaw_/test_no_spin_and_spin.py 3.26.0-1/ase/test/calculator/gpaw_/test_no_spin_and_spin.py
--- 3.24.0-1/ase/test/calculator/gpaw_/test_no_spin_and_spin.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/gpaw_/test_no_spin_and_spin.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import io
@@ -11,13 +12,16 @@ from ase.build import molecule
 def test_no_spin_and_spin(factory):
     txt = 'out.txt'
 
-    calculator = factory.calc(mode='fd', h=0.3, txt=txt)
-    atoms = molecule('H2', calculator=calculator)
-    atoms.center(vacuum=3)
-    atoms.get_potential_energy()
-    atoms.set_initial_magnetic_moments([0.5, 0.5])
-    calculator.set(charge=1)
-    atoms.get_potential_energy()
+    with open(txt, 'w') as txt_fd:
+        calculator = factory.calc(mode='fd', h=0.3, txt=txt_fd)
+        atoms = molecule('H2', calculator=calculator)
+        atoms.center(vacuum=3)
+        atoms.get_potential_energy()
+        atoms.set_initial_magnetic_moments([0.5, 0.5])
+        calculator = calculator.new(charge=1, txt=txt_fd)
+        atoms.calc = calculator
+        # calculator.set(charge=1)
+        atoms.get_potential_energy()
 
     # read again
     t = io.read(txt, index=':')
diff -pruN 3.24.0-1/ase/test/calculator/gpaw_/test_read_text_output.py 3.26.0-1/ase/test/calculator/gpaw_/test_read_text_output.py
--- 3.24.0-1/ase/test/calculator/gpaw_/test_read_text_output.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/gpaw_/test_read_text_output.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import io
 from ase.calculators.singlepoint import SinglePointDFTCalculator
 
diff -pruN 3.24.0-1/ase/test/calculator/gpaw_/test_wannier.py 3.26.0-1/ase/test/calculator/gpaw_/test_wannier.py
--- 3.24.0-1/ase/test/calculator/gpaw_/test_wannier.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/gpaw_/test_wannier.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from functools import partial
 
 import numpy as np
@@ -148,6 +149,7 @@ def wan(rng, h2_calculator):
         rng=rng,
         full_calc=False,
         std_calc=True,
+        symmetry='off'
     ):
         """
         Generate a Wannier object.
@@ -178,7 +180,7 @@ def wan(rng, h2_calculator):
                 gpts=gpts,
                 nbands=nwannier,
                 kpts=kpts,
-                symmetry='off',
+                symmetry=symmetry,
                 txt=None
             )
 
@@ -542,7 +544,8 @@ def test_distances(wan, h2_calculator):
     dist1_ww = wanf.distances([1, 1, 1])
     for i in range(nwannier):
         assert dist_ww[i, i] == pytest.approx(0)
-        assert dist1_ww[i, i] == pytest.approx(np.linalg.norm(atoms.cell.array))
+        assert dist1_ww[i, i] == pytest.approx(
+            np.linalg.norm(atoms.cell.array))
         for j in range(i + 1, nwannier):
             assert dist_ww[i, j] == dist_ww[j, i]
             assert dist_ww[i, j] == \
@@ -571,8 +574,10 @@ def test_get_hopping_random(wan, rng):
     hop1_ww = wanf.get_hopping([1, 1, 1])
     for i in range(nwannier):
         for j in range(i + 1, nwannier):
-            assert np.abs(hop0_ww[i, j]) == pytest.approx(np.abs(hop0_ww[j, i]))
-            assert np.abs(hop1_ww[i, j]) == pytest.approx(np.abs(hop1_ww[j, i]))
+            assert np.abs(hop0_ww[i, j]) == pytest.approx(
+                np.abs(hop0_ww[j, i]))
+            assert np.abs(hop1_ww[i, j]) == pytest.approx(
+                np.abs(hop1_ww[j, i]))
 
 
 def test_get_hamiltonian_bloch(wan):
@@ -813,3 +818,9 @@ def test_spread_contributions(wan):
     test_values_w = wan1._spread_contributions()
     ref_values_w = [2.28535569, 0.04660427]
     assert test_values_w == pytest.approx(ref_values_w, abs=1e-4)
+
+
+def test_symmetry_asserterror(wan):
+    sym = {}
+    with pytest.raises(RuntimeError, match='K-point symmetry*'):
+        wan(kpts=(4, 1, 1), symmetry=sym)
diff -pruN 3.24.0-1/ase/test/calculator/gromacs/test_gromacs.py 3.26.0-1/ase/test/calculator/gromacs/test_gromacs.py
--- 3.24.0-1/ase/test/calculator/gromacs/test_gromacs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/gromacs/test_gromacs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """ test run for gromacs calculator """
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/kim/test_cutoff_skin.py 3.26.0-1/ase/test/calculator/kim/test_cutoff_skin.py
--- 3.24.0-1/ase/test/calculator/kim/test_cutoff_skin.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/kim/test_cutoff_skin.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/kim/test_energy_forces_stress.py 3.26.0-1/ase/test/calculator/kim/test_energy_forces_stress.py
--- 3.24.0-1/ase/test/calculator/kim/test_energy_forces_stress.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/kim/test_energy_forces_stress.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 from pytest import mark
 
diff -pruN 3.24.0-1/ase/test/calculator/kim/test_modify_parameters.py 3.26.0-1/ase/test/calculator/kim/test_modify_parameters.py
--- 3.24.0-1/ase/test/calculator/kim/test_modify_parameters.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/kim/test_modify_parameters.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 from pytest import mark
diff -pruN 3.24.0-1/ase/test/calculator/kim/test_multi_neighlist.py 3.26.0-1/ase/test/calculator/kim/test_multi_neighlist.py
--- 3.24.0-1/ase/test/calculator/kim/test_multi_neighlist.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/kim/test_multi_neighlist.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 from pytest import mark
 
diff -pruN 3.24.0-1/ase/test/calculator/kim/test_relax.py 3.26.0-1/ase/test/calculator/kim/test_relax.py
--- 3.24.0-1/ase/test/calculator/kim/test_relax.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/kim/test_relax.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 from pytest import mark
 
diff -pruN 3.24.0-1/ase/test/calculator/kim/test_single_atom.py 3.26.0-1/ase/test/calculator/kim/test_single_atom.py
--- 3.24.0-1/ase/test/calculator/kim/test_single_atom.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/kim/test_single_atom.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from pytest import mark
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/calculator/kim/test_update_coords.py 3.26.0-1/ase/test/calculator/kim/test_update_coords.py
--- 3.24.0-1/ase/test/calculator/kim/test_update_coords.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/kim/test_update_coords.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 from pytest import mark
 
diff -pruN 3.24.0-1/ase/test/calculator/kim/test_update_neighbor_parameters.py 3.26.0-1/ase/test/calculator/kim/test_update_neighbor_parameters.py
--- 3.24.0-1/ase/test/calculator/kim/test_update_neighbor_parameters.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/kim/test_update_neighbor_parameters.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 from pytest import mark
diff -pruN 3.24.0-1/ase/test/calculator/lammps/test_prism.py 3.26.0-1/ase/test/calculator/lammps/test_prism.py
--- 3.24.0-1/ase/test/calculator/lammps/test_prism.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammps/test_prism.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test Prism"""
 from math import sqrt
 
diff -pruN 3.24.0-1/ase/test/calculator/lammpslib/conftest.py 3.26.0-1/ase/test/calculator/lammpslib/conftest.py
--- 3.24.0-1/ase/test/calculator/lammpslib/conftest.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpslib/conftest.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 
diff -pruN 3.24.0-1/ase/test/calculator/lammpslib/test_Pt_stress_cellopt.py 3.26.0-1/ase/test/calculator/lammpslib/test_Pt_stress_cellopt.py
--- 3.24.0-1/ase/test/calculator/lammpslib/test_Pt_stress_cellopt.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpslib/test_Pt_stress_cellopt.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 from numpy.testing import assert_allclose
diff -pruN 3.24.0-1/ase/test/calculator/lammpslib/test_lammpslib_change_cell_bcs.py 3.26.0-1/ase/test/calculator/lammpslib/test_lammpslib_change_cell_bcs.py
--- 3.24.0-1/ase/test/calculator/lammpslib/test_lammpslib_change_cell_bcs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpslib/test_lammpslib_change_cell_bcs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.lattice.cubic import FaceCenteredCubic
diff -pruN 3.24.0-1/ase/test/calculator/lammpslib/test_lammpslib_neighlist_bug.py 3.26.0-1/ase/test/calculator/lammpslib/test_lammpslib_neighlist_bug.py
--- 3.24.0-1/ase/test/calculator/lammpslib/test_lammpslib_neighlist_bug.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpslib/test_lammpslib_neighlist_bug.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/lammpslib/test_lammpslib_post_changebox_cmds.py 3.26.0-1/ase/test/calculator/lammpslib/test_lammpslib_post_changebox_cmds.py
--- 3.24.0-1/ase/test/calculator/lammpslib/test_lammpslib_post_changebox_cmds.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpslib/test_lammpslib_post_changebox_cmds.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/lammpslib/test_lammpslib_simple.py 3.26.0-1/ase/test/calculator/lammpslib/test_lammpslib_simple.py
--- 3.24.0-1/ase/test/calculator/lammpslib/test_lammpslib_simple.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpslib/test_lammpslib_simple.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/lammpslib/test_lammpslib_small_nonperiodic.py 3.26.0-1/ase/test/calculator/lammpslib/test_lammpslib_small_nonperiodic.py
--- 3.24.0-1/ase/test/calculator/lammpslib/test_lammpslib_small_nonperiodic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpslib/test_lammpslib_small_nonperiodic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/lammpsrun/test_Ar_minimize.py 3.26.0-1/ase/test/calculator/lammpsrun/test_Ar_minimize.py
--- 3.24.0-1/ase/test/calculator/lammpsrun/test_Ar_minimize.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpsrun/test_Ar_minimize.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 from numpy.testing import assert_allclose
 
diff -pruN 3.24.0-1/ase/test/calculator/lammpsrun/test_NaCl_minimize.py 3.26.0-1/ase/test/calculator/lammpsrun/test_NaCl_minimize.py
--- 3.24.0-1/ase/test/calculator/lammpsrun/test_NaCl_minimize.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpsrun/test_NaCl_minimize.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 from numpy.testing import assert_allclose
 
diff -pruN 3.24.0-1/ase/test/calculator/lammpsrun/test_Pt_md_constraints_multistep.py 3.26.0-1/ase/test/calculator/lammpsrun/test_Pt_md_constraints_multistep.py
--- 3.24.0-1/ase/test/calculator/lammpsrun/test_Pt_md_constraints_multistep.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpsrun/test_Pt_md_constraints_multistep.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 from numpy.testing import assert_allclose
 
diff -pruN 3.24.0-1/ase/test/calculator/lammpsrun/test_Pt_stress_cellopt.py 3.26.0-1/ase/test/calculator/lammpsrun/test_Pt_stress_cellopt.py
--- 3.24.0-1/ase/test/calculator/lammpsrun/test_Pt_stress_cellopt.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpsrun/test_Pt_stress_cellopt.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 from numpy.testing import assert_allclose
diff -pruN 3.24.0-1/ase/test/calculator/lammpsrun/test_lammps_units.py 3.26.0-1/ase/test/calculator/lammpsrun/test_lammps_units.py
--- 3.24.0-1/ase/test/calculator/lammpsrun/test_lammps_units.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpsrun/test_lammps_units.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import pi
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/lammpsrun/test_no_data_file_wrap.py 3.26.0-1/ase/test/calculator/lammpsrun/test_no_data_file_wrap.py
--- 3.24.0-1/ase/test/calculator/lammpsrun/test_no_data_file_wrap.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/lammpsrun/test_no_data_file_wrap.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.atoms import Atoms
diff -pruN 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_bands.py 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_bands.py
--- 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_bands.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_bands.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_broken_symmetry.py 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_broken_symmetry.py
--- 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_broken_symmetry.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_broken_symmetry.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Check if we can deal with spin-broken symmetries."""
 import pytest
 from numpy import array
diff -pruN 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_cmdline.py 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_cmdline.py
--- 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_cmdline.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_cmdline.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 from numpy.testing import assert_allclose
 
diff -pruN 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_eigenvalues.py 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_eigenvalues.py
--- 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_eigenvalues.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_eigenvalues.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_h3o2m.py 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_h3o2m.py
--- 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_h3o2m.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_h3o2m.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import cos, radians, sin
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_multitask.py 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_multitask.py
--- 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_multitask.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_multitask.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for the NWChem computations which use more than one task"""
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_parser.py 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_parser.py
--- 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_parser.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_parser.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.io.nwchem.parser import _pattern_test_data
 
 
diff -pruN 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_runmany.py 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_runmany.py
--- 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_runmany.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_runmany.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 from numpy.testing import assert_allclose
 
diff -pruN 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_spin_symmetry.py 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_spin_symmetry.py
--- 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_spin_symmetry.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_spin_symmetry.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Check if spin-symmetry is conserved"""
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_stress.py 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_stress.py
--- 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_stress.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_stress.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 from numpy.testing import assert_allclose
 
diff -pruN 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_version.py 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_version.py
--- 3.24.0-1/ase/test/calculator/nwchem/test_nwchem_version.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/nwchem/test_nwchem_version.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 
diff -pruN 3.24.0-1/ase/test/calculator/octopus/test_big.py 3.26.0-1/ase/test/calculator/octopus/test_big.py
--- 3.24.0-1/ase/test/calculator/octopus/test_big.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/octopus/test_big.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/calculator/onetep/test_onetep.py 3.26.0-1/ase/test/calculator/onetep/test_onetep.py
--- 3.24.0-1/ase/test/calculator/onetep/test_onetep.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/onetep/test_onetep.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from os.path import isfile, join
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/calculator/openmx/test_md.py 3.26.0-1/ase/test/calculator/openmx/test_md.py
--- 3.24.0-1/ase/test/calculator/openmx/test_md.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/openmx/test_md.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/openmx/test_version.py 3.26.0-1/ase/test/calculator/openmx/test_version.py
--- 3.24.0-1/ase/test/calculator/openmx/test_version.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/openmx/test_version.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.calculators.openmx.openmx import parse_omx_version
 
 sample_output = """\
diff -pruN 3.24.0-1/ase/test/calculator/plumed/test_plumed.py 3.26.0-1/ase/test/calculator/plumed/test_plumed.py
--- 3.24.0-1/ase/test/calculator/plumed/test_plumed.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/plumed/test_plumed.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 from pytest import approx
diff -pruN 3.24.0-1/ase/test/calculator/psi4/test_psi4_HF_3_21G.py 3.26.0-1/ase/test/calculator/psi4/test_psi4_HF_3_21G.py
--- 3.24.0-1/ase/test/calculator/psi4/test_psi4_HF_3_21G.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/psi4/test_psi4_HF_3_21G.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 from numpy.testing import assert_allclose
 
diff -pruN 3.24.0-1/ase/test/calculator/qchem/test_qchem_calculator.py 3.26.0-1/ase/test/calculator/qchem/test_qchem_calculator.py
--- 3.24.0-1/ase/test/calculator/qchem/test_qchem_calculator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/qchem/test_qchem_calculator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/calculator/siesta/lrtddft/test_siesta_lrtddft.py 3.26.0-1/ase/test/calculator/siesta/lrtddft/test_siesta_lrtddft.py
--- 3.24.0-1/ase/test/calculator/siesta/lrtddft/test_siesta_lrtddft.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/lrtddft/test_siesta_lrtddft.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/siesta/lrtddft/test_siesta_raman.py 3.26.0-1/ase/test/calculator/siesta/lrtddft/test_siesta_raman.py
--- 3.24.0-1/ase/test/calculator/siesta/lrtddft/test_siesta_raman.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/lrtddft/test_siesta_raman.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_bandstructure.py 3.26.0-1/ase/test/calculator/siesta/test_bandstructure.py
--- 3.24.0-1/ase/test/calculator/siesta/test_bandstructure.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_bandstructure.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_dos.py 3.26.0-1/ase/test/calculator/siesta/test_dos.py
--- 3.24.0-1/ase/test/calculator/siesta/test_dos.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_dos.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_fdf_io.py 3.26.0-1/ase/test/calculator/siesta/test_fdf_io.py
--- 3.24.0-1/ase/test/calculator/siesta/test_fdf_io.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_fdf_io.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_scripts/CH4/script.py 3.26.0-1/ase/test/calculator/siesta/test_scripts/CH4/script.py
--- 3.24.0-1/ase/test/calculator/siesta/test_scripts/CH4/script.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_scripts/CH4/script.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_scripts/Charge/script.py 3.26.0-1/ase/test/calculator/siesta/test_scripts/Charge/script.py
--- 3.24.0-1/ase/test/calculator/siesta/test_scripts/Charge/script.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_scripts/Charge/script.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # In this script the Virtual Crystal approximation is used to model
 # a stronger affinity for positive charge on the H atoms.
 # This could model interaction with other molecules not explicitly
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_scripts/H2/script.py 3.26.0-1/ase/test/calculator/siesta/test_scripts/H2/script.py
--- 3.24.0-1/ase/test/calculator/siesta/test_scripts/H2/script.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_scripts/H2/script.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.siesta import Siesta
 from ase.calculators.siesta.parameters import PAOBasisBlock, Species
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_scripts/Na8/script.py 3.26.0-1/ase/test/calculator/siesta/test_scripts/Na8/script.py
--- 3.24.0-1/ase/test/calculator/siesta/test_scripts/Na8/script.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_scripts/Na8/script.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Example, in order to run you must place a pseudopotential 'Na.psf' in
 the folder"""
 
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_scripts/Si/script.py 3.26.0-1/ase/test/calculator/siesta/test_scripts/Si/script.py
--- 3.24.0-1/ase/test/calculator/siesta/test_scripts/Si/script.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_scripts/Si/script.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.siesta import Siesta
 from ase.units import Ry
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_siesta_read_eigenvalues.py 3.26.0-1/ase/test/calculator/siesta/test_siesta_read_eigenvalues.py
--- 3.24.0-1/ase/test/calculator/siesta/test_siesta_read_eigenvalues.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_siesta_read_eigenvalues.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_siesta_version.py 3.26.0-1/ase/test/calculator/siesta/test_siesta_version.py
--- 3.24.0-1/ase/test/calculator/siesta/test_siesta_version.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_siesta_version.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.calculators.siesta.siesta import parse_siesta_version
 
 
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_siesta_zmat.py 3.26.0-1/ase/test/calculator/siesta/test_siesta_zmat.py
--- 3.24.0-1/ase/test/calculator/siesta/test_siesta_zmat.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_siesta_zmat.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for Zmatrix"""
 import os
 
diff -pruN 3.24.0-1/ase/test/calculator/siesta/test_write_input.py 3.26.0-1/ase/test/calculator/siesta/test_write_input.py
--- 3.24.0-1/ase/test/calculator/siesta/test_write_input.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/siesta/test_write_input.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test write_input"""
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/socketio/test_h2_stiffness.py 3.26.0-1/ase/test/calculator/socketio/test_h2_stiffness.py
--- 3.24.0-1/ase/test/calculator/socketio/test_h2_stiffness.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/socketio/test_h2_stiffness.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/socketio/test_integration.py 3.26.0-1/ase/test/calculator/socketio/test_integration.py
--- 3.24.0-1/ase/test/calculator/socketio/test_integration.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/socketio/test_integration.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/socketio/test_ipi_protocol_bfgs.py 3.26.0-1/ase/test/calculator/socketio/test_ipi_protocol_bfgs.py
--- 3.24.0-1/ase/test/calculator/socketio/test_ipi_protocol_bfgs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/socketio/test_ipi_protocol_bfgs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 import sys
 import threading
diff -pruN 3.24.0-1/ase/test/calculator/socketio/test_python_interface.py 3.26.0-1/ase/test/calculator/socketio/test_python_interface.py
--- 3.24.0-1/ase/test/calculator/socketio/test_python_interface.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/socketio/test_python_interface.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/calculator/socketio/test_socket_io_mpi_line.py 3.26.0-1/ase/test/calculator/socketio/test_socket_io_mpi_line.py
--- 3.24.0-1/ase/test/calculator/socketio/test_socket_io_mpi_line.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/socketio/test_socket_io_mpi_line.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.calculators.abinit import AbinitTemplate
 from ase.calculators.espresso import EspressoTemplate
 from ase.config import Config
diff -pruN 3.24.0-1/ase/test/calculator/test_al.py 3.26.0-1/ase/test/calculator/test_al.py
--- 3.24.0-1/ase/test/calculator/test_al.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_al.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/calculator/test_bond_polarizability.py 3.26.0-1/ase/test/calculator/test_bond_polarizability.py
--- 3.24.0-1/ase/test/calculator/test_bond_polarizability.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_bond_polarizability.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/test_calculator.py 3.26.0-1/ase/test/calculator/test_calculator.py
--- 3.24.0-1/ase/test/calculator/test_calculator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_calculator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from pathlib import Path
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/test_ch4_energy.py 3.26.0-1/ase/test/calculator/test_ch4_energy.py
--- 3.24.0-1/ase/test/calculator/test_ch4_energy.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_ch4_energy.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/calculator/test_command.py 3.26.0-1/ase/test/calculator/test_command.py
--- 3.24.0-1/ase/test/calculator/test_command.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_command.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 import subprocess
 
@@ -51,7 +52,6 @@ calculators = {
     'dftb': {},
     'dftd3': {},
     'dmol': {},
-    'elk': {},
     'gamess_us': {},
     'gaussian': {},
     'gromacs': {},
@@ -143,7 +143,6 @@ envvars = {
     'dftb': 'DFTB_COMMAND',
     'dftd3': 'ASE_DFTD3_COMMAND',
     'dmol': 'DMOL_COMMAND',  # XXX Crashes when it runs along other tests
-    'elk': 'ASE_ELK_COMMAND',
     'gamess_us': 'ASE_GAMESSUS_COMMAND',
     'gaussian': 'ASE_GAUSSIAN_COMMAND',
     'gromacs': 'ASE_GROMACS_COMMAND',
@@ -251,7 +250,6 @@ default_commands = {
     'cp2k': 'cp2k_shell',
     'dftb': 'dftb+ > dftb.out',
     'dftd3': f'dftd3 {dftd3_boilerplate}'.split(),
-    'elk': 'elk > elk.out',
     'gamess_us': 'rungms gamess_us.inp > gamess_us.log 2> gamess_us.err',
     'gaussian': 'g16 < Gaussian.com > Gaussian.log',
     'gulp': 'gulp < gulp.gin > gulp.got',
diff -pruN 3.24.0-1/ase/test/calculator/test_dftd3.py 3.26.0-1/ase/test/calculator/test_dftd3.py
--- 3.24.0-1/ase/test/calculator/test_dftd3.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_dftd3.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/test_eam.py 3.26.0-1/ase/test/calculator/test_eam.py
--- 3.24.0-1/ase/test/calculator/test_eam.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_eam.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 from scipy.interpolate import InterpolatedUnivariateSpline as spline
 
diff -pruN 3.24.0-1/ase/test/calculator/test_eam_run.py 3.26.0-1/ase/test/calculator/test_eam_run.py
--- 3.24.0-1/ase/test/calculator/test_eam_run.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_eam_run.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,7 +1,12 @@
+# fmt: off
 import numpy as np
 import pytest
 
 from ase.build import bulk, fcc111
+from ase.calculators.fd import (
+    calculate_numerical_forces,
+    calculate_numerical_stress,
+)
 
 
 @pytest.mark.calculator('eam')
@@ -34,3 +39,13 @@ def test_read_potential(factory, potenti
     atoms = bulk(element)
     atoms.calc = calc
     atoms.get_potential_energy()
+
+    # test forces against numerical forces
+    forces = atoms.get_forces()
+    numerical_forces = calculate_numerical_forces(atoms, eps=1e-5)
+    np.testing.assert_allclose(forces, numerical_forces, atol=1e-5)
+
+    # test stress against numerical stress
+    stress = atoms.get_stress()
+    numerical_stress = calculate_numerical_stress(atoms, eps=1e-5)
+    np.testing.assert_allclose(stress, numerical_stress, atol=1e-5)
diff -pruN 3.24.0-1/ase/test/calculator/test_fd.py 3.26.0-1/ase/test/calculator/test_fd.py
--- 3.24.0-1/ase/test/calculator/test_fd.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_fd.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,15 +1,24 @@
+"""Tests for `FiniteDifferenceCalculator`."""
+
 import numpy as np
+import pytest
 
+from ase import Atoms
 from ase.build import bulk
 from ase.calculators.emt import EMT
 from ase.calculators.fd import FiniteDifferenceCalculator
 
 
-def test_fd():
-    """Test `FiniteDifferenceCalculator`."""
+@pytest.fixture(name='atoms')
+def fixture_atoms() -> Atoms:
+    """Make a fixture of atoms."""
     atoms = bulk('Cu', cubic=True)
     atoms.rattle(0.1)
+    return atoms
 
+
+def test_fd(atoms: Atoms) -> None:
+    """Test `FiniteDifferenceCalculator`."""
     atoms.calc = EMT()
     energy_analytical = atoms.get_potential_energy()
     forces_analytical = atoms.get_forces()
@@ -29,3 +38,27 @@ def test_fd():
 
     np.testing.assert_allclose(forces_numerical, forces_analytical)
     np.testing.assert_allclose(stress_numerical, stress_analytical)
+
+
+def test_analytical_forces(atoms: Atoms) -> None:
+    """Test if analytical forces are available."""
+    atoms.calc = EMT()
+    forces_ref = atoms.get_forces()
+
+    atoms.calc = FiniteDifferenceCalculator(EMT(), eps_disp=None)
+    forces_fdc = atoms.get_forces()
+
+    # check if forces are *exactly* equal to `analytical`
+    np.testing.assert_array_equal(forces_fdc, forces_ref)
+
+
+def test_analytical_stress(atoms: Atoms) -> None:
+    """Test if analytical stress is available."""
+    atoms.calc = EMT()
+    stress_ref = atoms.get_stress()
+
+    atoms.calc = FiniteDifferenceCalculator(EMT(), eps_strain=None)
+    stress_fdc = atoms.get_stress()
+
+    # check if stress is *exactly* equal to `analytical`
+    np.testing.assert_array_equal(stress_fdc, stress_ref)
diff -pruN 3.24.0-1/ase/test/calculator/test_gulp.py 3.26.0-1/ase/test/calculator/test_gulp.py
--- 3.24.0-1/ase/test/calculator/test_gulp.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_gulp.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/calculator/test_gulp_opt.py 3.26.0-1/ase/test/calculator/test_gulp_opt.py
--- 3.24.0-1/ase/test/calculator/test_gulp_opt.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_gulp_opt.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk, molecule
diff -pruN 3.24.0-1/ase/test/calculator/test_h2.py 3.26.0-1/ase/test/calculator/test_h2.py
--- 3.24.0-1/ase/test/calculator/test_h2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_h2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/calculator/test_h2_bond_force.py 3.26.0-1/ase/test/calculator/test_h2_bond_force.py
--- 3.24.0-1/ase/test/calculator/test_h2_bond_force.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_h2_bond_force.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/test_h2morse.py 3.26.0-1/ase/test/calculator/test_h2morse.py
--- 3.24.0-1/ase/test/calculator/test_h2morse.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_h2morse.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/test_harmonic.py 3.26.0-1/ase/test/calculator/test_harmonic.py
--- 3.24.0-1/ase/test/calculator/test_harmonic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_harmonic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 from numpy.testing import assert_array_almost_equal
diff -pruN 3.24.0-1/ase/test/calculator/test_lj.py 3.26.0-1/ase/test/calculator/test_lj.py
--- 3.24.0-1/ase/test/calculator/test_lj.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_lj.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/test_mopac.py 3.26.0-1/ase/test/calculator/test_mopac.py
--- 3.24.0-1/ase/test/calculator/test_mopac.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_mopac.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 from numpy.testing import assert_allclose
 
diff -pruN 3.24.0-1/ase/test/calculator/test_mopac_version.py 3.26.0-1/ase/test/calculator/test_mopac_version.py
--- 3.24.0-1/ase/test/calculator/test_mopac_version.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_mopac_version.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 snippets = {
diff -pruN 3.24.0-1/ase/test/calculator/test_morse.py 3.26.0-1/ase/test/calculator/test_morse.py
--- 3.24.0-1/ase/test/calculator/test_morse.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_morse.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,6 @@
+# fmt: off
 import numpy as np
+import pytest
 from scipy.optimize import check_grad
 
 from ase import Atoms
@@ -50,3 +52,15 @@ def test_forces_and_stress():
     stress = atoms.get_stress()
     numerical_stress = calculate_numerical_stress(atoms, eps=1e-5)
     np.testing.assert_allclose(stress, numerical_stress, atol=1e-5)
+
+
+def fake_neighbor_list(*args, **kwargs):
+    raise RuntimeError('test_neighbor_list')
+
+
+def test_override_neighbor_list():
+    with pytest.raises(RuntimeError, match='test_neighbor_list'):
+        atoms = bulk('Cu', cubic=True)
+        atoms.calc = MorsePotential(A=4.0, epsilon=1.0, r0=2.55,
+                                    neighbor_list=fake_neighbor_list)
+        _ = atoms.get_potential_energy()
diff -pruN 3.24.0-1/ase/test/calculator/test_orca.py 3.26.0-1/ase/test/calculator/test_orca.py
--- 3.24.0-1/ase/test/calculator/test_orca.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_orca.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import re
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/calculator/test_orca_qmmm.py 3.26.0-1/ase/test/calculator/test_orca_qmmm.py
--- 3.24.0-1/ase/test/calculator/test_orca_qmmm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_orca_qmmm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.calculators.qmmm import EIQMMM, LJInteractions
diff -pruN 3.24.0-1/ase/test/calculator/test_polarizability.py 3.26.0-1/ase/test/calculator/test_polarizability.py
--- 3.24.0-1/ase/test/calculator/test_polarizability.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_polarizability.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.calculators.excitation_list import polarizability
diff -pruN 3.24.0-1/ase/test/calculator/test_si_stress.py 3.26.0-1/ase/test/calculator/test_si_stress.py
--- 3.24.0-1/ase/test/calculator/test_si_stress.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_si_stress.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/test_subprocess_calculator.py 3.26.0-1/ase/test/calculator/test_subprocess_calculator.py
--- 3.24.0-1/ase/test/calculator/test_subprocess_calculator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_subprocess_calculator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from pathlib import Path
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/calculator/test_tersoff.py 3.26.0-1/ase/test/calculator/test_tersoff.py
--- 3.24.0-1/ase/test/calculator/test_tersoff.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_tersoff.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,249 @@
+"""Tests for ``Tersoff``."""
+
+import numpy as np
+import pytest
+
+from ase import Atoms
+from ase.build import bulk
+from ase.calculators.calculator import PropertyNotImplementedError
+from ase.calculators.fd import (
+    calculate_numerical_forces,
+    calculate_numerical_stress,
+)
+from ase.calculators.tersoff import Tersoff, TersoffParameters
+
+
+@pytest.fixture
+def si_parameters():
+    """Fixture providing the Silicon parameters.
+
+    Parameters taken from: Tersoff, Phys Rev B, 37, 6991 (1988)
+    """
+    return {
+        ('Si', 'Si', 'Si'): TersoffParameters(
+            A=3264.7,
+            B=95.373,
+            lambda1=3.2394,
+            lambda2=1.3258,
+            lambda3=1.3258,
+            beta=0.33675,
+            gamma=1.00,
+            m=3.00,
+            n=22.956,
+            c=4.8381,
+            d=2.0417,
+            h=0.0000,
+            R=3.00,
+            D=0.20,
+        )
+    }
+
+
+@pytest.fixture(name='atoms_si')
+def fixture_atoms_si(
+    si_parameters: dict[tuple[str, str, str], TersoffParameters],
+) -> Atoms:
+    """Make Atoms for Si with a small displacement on the first atom."""
+    atoms = bulk('Si', a=5.43, cubic=True)
+
+    # pertubate first atom to get substantial forces
+    atoms.positions[0] += [0.03, 0.02, 0.01]
+
+    atoms.calc = Tersoff(si_parameters)
+
+    return atoms
+
+
+def test_initialize_from_params_from_dict(si_parameters):
+    """Test initializing Tersoff calculator from dictionary of parameters."""
+    calc = Tersoff(si_parameters)
+    assert calc.parameters == si_parameters
+    diamond = bulk('Si', 'diamond', a=5.43)
+    diamond.calc = calc
+    potential_energy = diamond.get_potential_energy()
+    np.testing.assert_allclose(potential_energy, -9.260818674314585, atol=1e-8)
+
+
+def test_set_parameters(
+    si_parameters: dict[tuple[str, str, str], TersoffParameters],
+) -> None:
+    """Test updating parameters of the Tersoff calculator."""
+    calc = Tersoff(si_parameters)
+    key = ('Si', 'Si', 'Si')
+
+    calc.set_parameters(key, m=2.0)
+    assert calc.parameters[key].m == 2.0
+
+    calc.set_parameters(key, R=2.90, D=0.25)
+    assert calc.parameters[key].R == 2.90
+    assert calc.parameters[key].D == 0.25
+
+    new_params = TersoffParameters(
+        m=si_parameters[key].m,
+        gamma=si_parameters[key].gamma,
+        lambda3=si_parameters[key].lambda3,
+        c=si_parameters[key].c,
+        d=si_parameters[key].d,
+        h=si_parameters[key].h,
+        n=si_parameters[key].n,
+        beta=si_parameters[key].beta,
+        lambda2=si_parameters[key].lambda2,
+        B=si_parameters[key].B,
+        R=3.00,  # Reset cutoff radius
+        D=si_parameters[key].D,
+        lambda1=si_parameters[key].lambda1,
+        A=si_parameters[key].A,
+    )
+    calc.set_parameters(key, params=new_params)
+    assert calc.parameters[key] == new_params
+
+
+def test_isolated_atom(si_parameters: dict) -> None:
+    """Test if an isolated atom can be computed correctly."""
+    atoms = Atoms('Si')
+    atoms.calc = Tersoff(si_parameters)
+    energy = atoms.get_potential_energy()
+    energies = atoms.get_potential_energies()
+    forces = atoms.get_forces()
+    np.testing.assert_almost_equal(energy, 0.0)
+    np.testing.assert_allclose(energies, [0.0], rtol=1e-5)
+    np.testing.assert_allclose(forces, [[0.0] * 3], rtol=1e-5)
+    with pytest.raises(PropertyNotImplementedError):
+        atoms.get_stress()
+
+
+def test_unary(atoms_si: Atoms) -> None:
+    """Test if energy, forces, and stress of a unary system agree with LAMMPS.
+
+    The reference values are obtained in the following way.
+
+    >>> from ase.calculators.lammpslib import LAMMPSlib
+    >>>
+    >>> atoms = bulk('Si', a=5.43, cubic=True)
+    >>> atoms.positions[0] += [0.03, 0.02, 0.01]
+    >>> lmpcmds = ['pair_style tersoff', 'pair_coeff * * Si.tersoff Si']
+    >>> atoms.calc = LAMMPSlib(lmpcmds=lmpcmds)
+    >>> energy = atoms.get_potential_energy()
+    >>> energies = atoms.get_potential_energies()
+    >>> forces = atoms.get_forces()
+    >>> stress = atoms.get_stress()
+
+    """
+    energy_ref = -37.03237572778589
+    energies_ref = [
+        -4.62508202,
+        -4.62242901,
+        -4.63032346,
+        -4.63028909,
+        -4.63037555,
+        -4.63147495,
+        -4.63040683,
+        -4.63199482,
+    ]
+    forces_ref = [
+        [-4.63805736e-01, -3.17112011e-01, -1.79345801e-01],
+        [+2.34142607e-01, +2.29060580e-01, +2.24142706e-01],
+        [-2.79544489e-02, +1.31289732e-03, +3.99485914e-04],
+        [+1.85144670e-02, +1.48017753e-02, +8.47421196e-03],
+        [+2.06558877e-03, -1.86613107e-02, +3.98039278e-04],
+        [+8.68756690e-02, -5.15405628e-02, +7.32472691e-02],
+        [+2.06388309e-03, +1.30960793e-03, -9.30764103e-03],
+        [+1.48097970e-01, +1.40829025e-01, -1.18008270e-01],
+    ]
+    stress_ref = [
+        -0.00048610,
+        -0.00056779,
+        -0.00061684,
+        -0.00342602,
+        -0.00231541,
+        -0.00124569,
+    ]
+
+    energy = atoms_si.get_potential_energy()
+    energies = atoms_si.get_potential_energies()
+    forces = atoms_si.get_forces()
+    stress = atoms_si.get_stress()
+    np.testing.assert_almost_equal(energy, energy_ref)
+    np.testing.assert_allclose(energies, energies_ref, rtol=1e-5)
+    np.testing.assert_allclose(forces, forces_ref, rtol=1e-5)
+    np.testing.assert_allclose(stress, stress_ref, rtol=1e-5)
+
+
+def test_binary(datadir) -> None:
+    """Test if energy, forces, and stress of a binary system agree with LAMMPS.
+
+    The reference values are obtained in the following way.
+
+    >>> from ase.calculators.lammpslib import LAMMPSlib
+    >>>
+    >>> atoms = bulk('Si', a=5.43, cubic=True)
+    >>> atoms.symbols[1] = 'C'
+    >>> atoms.symbols[2] = 'C'
+    >>> atoms.positions[0] += [0.03, 0.02, 0.01]
+    >>> lmpcmds = ['pair_style tersoff', 'pair_coeff * * SiC.tersoff Si C']
+    >>> atoms.calc = LAMMPSlib(lmpcmds=lmpcmds)
+    >>> energy = atoms.get_potential_energy()
+    >>> energies = atoms.get_potential_energies()
+    >>> forces = atoms.get_forces()
+    >>> stress = atoms.get_stress()
+
+    """
+    atoms = bulk('Si', a=5.43, cubic=True)
+    atoms.symbols[1] = 'C'
+    atoms.symbols[2] = 'C'
+
+    # pertubate first atom to get substantial forces
+    atoms.positions[0] += [0.03, 0.02, 0.01]
+
+    potential_file = datadir / 'tersoff' / 'SiC.tersoff'
+    atoms.calc = Tersoff.from_lammps(potential_file)
+
+    energy_ref = -28.780184609451915
+    energies_ref = [
+        -4.33637575,
+        -2.02218449,
+        -1.80044260,
+        -4.12192108,
+        -4.12650203,
+        -4.12473794,
+        -4.12677193,
+        -4.12124880,
+    ]
+    forces_ref = [
+        [+6.40479511, +6.64830387, +6.83733140],
+        [+6.93259841, -7.29932178, -7.32986722],
+        [-7.09646214, +7.17384006, +7.15478087],
+        [-6.63906798, -6.62943844, -6.64034900],
+        [-6.48323569, +6.55237593, -6.52463929],
+        [+6.64668748, +6.56474714, -6.55390547],
+        [-6.47026652, -6.50615747, +6.53977908],
+        [+6.70495132, -6.50434930, +6.51686963],
+    ]
+    stress_ref = [
+        +0.35188635,
+        +0.35366730,
+        +0.35444532,
+        -0.11629806,
+        +0.11665436,
+        +0.11668285,
+    ]
+
+    energy = atoms.get_potential_energy()
+    energies = atoms.get_potential_energies()
+    forces = atoms.get_forces()
+    stress = atoms.get_stress()
+    np.testing.assert_almost_equal(energy, energy_ref)
+    np.testing.assert_allclose(energies, energies_ref, rtol=1e-5)
+    np.testing.assert_allclose(forces, forces_ref, rtol=1e-5)
+    np.testing.assert_allclose(stress, stress_ref, rtol=1e-5)
+
+
+def test_forces_and_stress(atoms_si: Atoms) -> None:
+    """Test if analytical forces and stress agree with numerical ones."""
+    forces = atoms_si.get_forces()
+    numerical_forces = calculate_numerical_forces(atoms_si, eps=1e-5)
+    np.testing.assert_allclose(forces, numerical_forces, atol=1e-5)
+
+    stress = atoms_si.get_stress()
+    numerical_stress = calculate_numerical_stress(atoms_si, eps=1e-5)
+    np.testing.assert_allclose(stress, numerical_stress, atol=1e-5)
diff -pruN 3.24.0-1/ase/test/calculator/test_traj.py 3.26.0-1/ase/test/calculator/test_traj.py
--- 3.24.0-1/ase/test/calculator/test_traj.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_traj.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/calculator/test_ts09.py 3.26.0-1/ase/test/calculator/test_ts09.py
--- 3.24.0-1/ase/test/calculator/test_ts09.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/test_ts09.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import io
diff -pruN 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_2h2o.py 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_2h2o.py
--- 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_2h2o.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_2h2o.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # type: ignore
 from numpy.linalg import norm
 
diff -pruN 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_H2.py 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_H2.py
--- 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_H2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_H2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # type: ignore
 import os.path
 
diff -pruN 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_au13.py 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_au13.py
--- 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_au13.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_au13.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # type: ignore
 import numpy as np
 
diff -pruN 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_calculator.py 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_calculator.py
--- 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_calculator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_calculator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # type: ignore
 import sys
 
diff -pruN 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_h2o.py 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_h2o.py
--- 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_h2o.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_h2o.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # type: ignore
 import numpy as np
 
diff -pruN 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_h3o2m.py 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_h3o2m.py
--- 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_h3o2m.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_h3o2m.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # type: ignore
 from math import cos, radians, sin
 
diff -pruN 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_optimizer.py 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_optimizer.py
--- 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_optimizer.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_optimizer.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # type: ignore
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_parameters.py 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_parameters.py
--- 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_parameters.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_parameters.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # type: ignore
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_qmmm.py 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_qmmm.py
--- 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_qmmm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_qmmm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # type: ignore
 from math import cos, pi, sin
 
diff -pruN 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_reader.py 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_reader.py
--- 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_reader.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_reader.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """unit tests for the turbomole reader module"""
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_statpt.py 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_statpt.py
--- 3.24.0-1/ase/test/calculator/turbomole/test_turbomole_statpt.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/turbomole/test_turbomole_statpt.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # type: ignore
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/vasp/__init__.py 3.26.0-1/ase/test/calculator/vasp/__init__.py
--- 3.24.0-1/ase/test/calculator/vasp/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/vasp/conftest.py 3.26.0-1/ase/test/calculator/vasp/conftest.py
--- 3.24.0-1/ase/test/calculator/vasp/conftest.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/conftest.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/vasp/filecmp_ignore_whitespace.py 3.26.0-1/ase/test/calculator/vasp/filecmp_ignore_whitespace.py
--- 3.24.0-1/ase/test/calculator/vasp/filecmp_ignore_whitespace.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/filecmp_ignore_whitespace.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import re
 
 
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_chgcar.py 3.26.0-1/ase/test/calculator/vasp/test_chgcar.py
--- 3.24.0-1/ase/test/calculator/vasp/test_chgcar.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_chgcar.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for CHG/CHGCAR."""
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_Al_volrelax.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_Al_volrelax.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_Al_volrelax.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_Al_volrelax.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_calculator.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_calculator.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_calculator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_calculator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test module for explicitly unittesting parts of the VASP calculator"""
 
 import os
@@ -112,6 +113,12 @@ def test_vasp_no_cell(testdir):
         atoms.get_total_energy()
 
 
+def test_vasp_kpoints_none(atoms, tmp_path, monkeypatch):
+    monkeypatch.chdir(tmp_path)
+    Vasp(kpts=None).write_kpoints(atoms=atoms)
+    assert not os.path.isfile('KPOINTS')
+
+
 def test_spinpol_vs_ispin():
     """Test if `spinpol` is consistent with `ispin`"""
     atoms = molecule("O2")
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_charge.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_charge.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_charge.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_charge.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_check_state.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_check_state.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_check_state.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_check_state.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_co.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_co.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_co.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_co.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_converge.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_converge.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_converge.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_converge.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from pathlib import Path
 
 from ase.calculators.vasp import Vasp
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_errors.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_errors.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_errors.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_errors.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test module for explicitly unittesting errors generated by VASP calculator"""
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_freq.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_freq.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_freq.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_freq.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_get_dos.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_get_dos.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_get_dos.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_get_dos.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
@@ -10,7 +11,7 @@ def test_vasp_Si_get_dos(factory):
     """
     Run VASP tests to ensure that the get_dos function works properly.
     This test is corresponding to the tutorial:
-    https://wiki.fysik.dtu.dk/ase/ase/calculators/vasp.html#density-of-states
+    https://ase-lib.org/ase/calculators/vasp.html#density-of-states
     This is conditional on the existence of the VASP_COMMAND or VASP_SCRIPT
     environment variables.
 
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_incar.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_incar.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_incar.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_incar.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # from os.path import join
 from unittest import mock
 
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_input.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_input.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_input.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_input.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+from io import StringIO
 from unittest import mock
 
 import numpy as np
@@ -7,8 +9,10 @@ from ase.build import bulk
 from ase.calculators.vasp.create_input import (
     GenerateVaspInput,
     _args_without_comment,
+    _calc_nelect_from_charge,
     _from_vasp_bool,
     _to_vasp_bool,
+    read_potcar_numbers_of_electrons,
 )
 
 
@@ -323,3 +327,23 @@ def test_bool(tmp_path, vaspinput_factor
         calc = vaspinput_factory(encut=100)
         with pytest.raises(ValueError):
             calc.read_incar(tmp_path / 'INCAR')
+
+
+def test_read_potcar_numbers_of_electrons() -> None:
+    """Test if the numbers of valence electrons are parsed correctly."""
+    # POTCAR lines publicly available
+    # https://www.vasp.at/wiki/index.php/POTCAR
+    lines = """\
+TITEL  = PAW_PBE Ti_pv 07Sep2000
+...
+...
+...
+POMASS =   47.880; ZVAL   =   10.000    mass and valenz
+"""
+    assert read_potcar_numbers_of_electrons(StringIO(lines)) == [('Ti', 10.0)]
+
+
+def test_calc_nelect_from_charge() -> None:
+    """Test if NELECT can be determined correctly."""
+    assert _calc_nelect_from_charge(None, None, 10.0) is None
+    assert _calc_nelect_from_charge(None, 4.0, 10.0) == 6.0
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_kpoints.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_kpoints.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_kpoints.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_kpoints.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 Check the many ways of specifying KPOINTS
 """
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_potcar.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_potcar.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_potcar.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_potcar.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_setup.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_setup.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_setup.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_setup.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.atoms import Atoms
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_wdir.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_wdir.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_wdir.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_wdir.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import pytest
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_vasp_xml.py 3.26.0-1/ase/test/calculator/vasp/test_vasp_xml.py
--- 3.24.0-1/ase/test/calculator/vasp/test_vasp_xml.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_vasp_xml.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 Run some VASP tests to ensure that the VASP calculator works. This
 is conditional on the existence of the VASP_COMMAND or VASP_SCRIPT
diff -pruN 3.24.0-1/ase/test/calculator/vasp/test_version.py 3.26.0-1/ase/test/calculator/vasp/test_version.py
--- 3.24.0-1/ase/test/calculator/vasp/test_version.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/calculator/vasp/test_version.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.calculators.vasp import get_vasp_version
 
 vasp_sample_header = """\
diff -pruN 3.24.0-1/ase/test/cell/test_cell.py 3.26.0-1/ase/test/cell/test_cell.py
--- 3.24.0-1/ase/test/cell/test_cell.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cell/test_cell.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/cell/test_cell_completion.py 3.26.0-1/ase/test/cell/test_cell_completion.py
--- 3.24.0-1/ase/test/cell/test_cell_completion.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cell/test_cell_completion.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.geometry.cell import complete_cell
diff -pruN 3.24.0-1/ase/test/cell/test_cell_conv.py 3.26.0-1/ase/test/cell/test_cell_conv.py
--- 3.24.0-1/ase/test/cell/test_cell_conv.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cell/test_cell_conv.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.geometry import cell_to_cellpar as c2p
diff -pruN 3.24.0-1/ase/test/cell/test_cell_uncompletion.py 3.26.0-1/ase/test/cell/test_cell_uncompletion.py
--- 3.24.0-1/ase/test/cell/test_cell_uncompletion.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cell/test_cell_uncompletion.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import itertools
 
 import pytest
diff -pruN 3.24.0-1/ase/test/cell/test_conventional_map.py 3.26.0-1/ase/test/cell/test_conventional_map.py
--- 3.24.0-1/ase/test/cell/test_conventional_map.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cell/test_conventional_map.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/cell/test_minkowski_reduce.py 3.26.0-1/ase/test/cell/test_minkowski_reduce.py
--- 3.24.0-1/ase/test/cell/test_minkowski_reduce.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cell/test_minkowski_reduce.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 from numpy.testing import assert_allclose, assert_almost_equal
diff -pruN 3.24.0-1/ase/test/cell/test_niggli.py 3.26.0-1/ase/test/cell/test_niggli.py
--- 3.24.0-1/ase/test/cell/test_niggli.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cell/test_niggli.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # Convert a selection of unit cells, both reasonable and unreasonable,
 # into their Niggli unit cell, and compare against the pre-computed values.
 # The tests and pre-computed values come from the program cctbx, in which
diff -pruN 3.24.0-1/ase/test/cell/test_niggli_ndim.py 3.26.0-1/ase/test/cell/test_niggli_ndim.py
--- 3.24.0-1/ase/test/cell/test_niggli_ndim.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cell/test_niggli_ndim.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import itertools
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/cell/test_niggli_op.py 3.26.0-1/ase/test/cell/test_niggli_op.py
--- 3.24.0-1/ase/test/cell/test_niggli_op.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cell/test_niggli_op.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.cell import Cell
diff -pruN 3.24.0-1/ase/test/cell/test_standard_form.py 3.26.0-1/ase/test/cell/test_standard_form.py
--- 3.24.0-1/ase/test/cell/test_standard_form.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cell/test_standard_form.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,19 +1,24 @@
+# fmt: off
 import numpy as np
 from numpy.testing import assert_allclose
 
 from ase.cell import Cell
+from ase.lattice import all_variants
 
 
 def test_standard_form():
-
     TOL = 1E-10
-    rng = np.random.RandomState(0)
-
-    for _ in range(20):
-        cell0 = rng.uniform(-1, 1, (3, 3))
+    for lat in all_variants():
+        cell0 = lat.tocell()
         for sign in [-1, 1]:
             cell = Cell(sign * cell0)
-            rcell, Q = cell.standard_form()
+            # lower triangular form
+            rcell, Q = cell.standard_form(form='lower')
             assert_allclose(rcell @ Q, cell, atol=TOL)
             assert_allclose(np.linalg.det(rcell), np.linalg.det(cell))
             assert_allclose(rcell.ravel()[[1, 2, 5]], 0, atol=TOL)
+            # upper triangular form
+            rcell, Q = cell.standard_form(form='upper')
+            assert_allclose(rcell @ Q, cell, atol=TOL)
+            assert_allclose(np.linalg.det(rcell), np.linalg.det(cell))
+            assert_allclose(rcell.ravel()[[3, 6, 7]], 0, atol=TOL)
diff -pruN 3.24.0-1/ase/test/cell/test_supercell.py 3.26.0-1/ase/test/cell/test_supercell.py
--- 3.24.0-1/ase/test/cell/test_supercell.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cell/test_supercell.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/cli/test_ag.py 3.26.0-1/ase/test/cli/test_ag.py
--- 3.24.0-1/ase/test/cli/test_ag.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cli/test_ag.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.io import write
 
diff -pruN 3.24.0-1/ase/test/cli/test_bandstructure.py 3.26.0-1/ase/test/cli/test_bandstructure.py
--- 3.24.0-1/ase/test/cli/test_bandstructure.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cli/test_bandstructure.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from pathlib import Path
 
 from ase.lattice import RHL
diff -pruN 3.24.0-1/ase/test/cli/test_bzplot.py 3.26.0-1/ase/test/cli/test_bzplot.py
--- 3.24.0-1/ase/test/cli/test_bzplot.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cli/test_bzplot.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/cli/test_complete.py 3.26.0-1/ase/test/cli/test_complete.py
--- 3.24.0-1/ase/test/cli/test_complete.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cli/test_complete.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Check that our tab-completion script has been updated."""
 from ase.cli.completion import path, update
 from ase.cli.main import commands
diff -pruN 3.24.0-1/ase/test/cli/test_convert.py 3.26.0-1/ase/test/cli/test_convert.py
--- 3.24.0-1/ase/test/cli/test_convert.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cli/test_convert.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk
 from ase.calculators.calculator import compare_atoms
 from ase.io import read, write
diff -pruN 3.24.0-1/ase/test/cli/test_diff.py 3.26.0-1/ase/test/cli/test_diff.py
--- 3.24.0-1/ase/test/cli/test_diff.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cli/test_diff.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import re
 
 import pytest
diff -pruN 3.24.0-1/ase/test/cli/test_dimensionality.py 3.26.0-1/ase/test/cli/test_dimensionality.py
--- 3.24.0-1/ase/test/cli/test_dimensionality.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cli/test_dimensionality.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 import ase.build
diff -pruN 3.24.0-1/ase/test/cli/test_exec.py 3.26.0-1/ase/test/cli/test_exec.py
--- 3.24.0-1/ase/test/cli/test_exec.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cli/test_exec.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk, molecule
diff -pruN 3.24.0-1/ase/test/cli/test_imports.py 3.26.0-1/ase/test/cli/test_imports.py
--- 3.24.0-1/ase/test/cli/test_imports.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cli/test_imports.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,5 @@
 """Check that plain cli doesn't execute too many imports."""
+
 import sys
 
 from ase.utils.checkimports import check_imports
@@ -12,9 +13,12 @@ def test_imports():
         'ase.calculators.(?!names).*',  # any calculator
     ]
     if sys.version_info >= (3, 10):
-        max_nonstdlib_module_count = 200  # this depends on the environment
+        max_nonstdlib_module_count = 350  # this depends on the environment
+        # Should get this to less than 200
     else:
         max_nonstdlib_module_count = None
-    check_imports("from ase.cli.main import main; main(args=[])",
-                  forbidden_modules=forbidden_modules,
-                  max_nonstdlib_module_count=max_nonstdlib_module_count)
+    check_imports(
+        'from ase.cli.main import main; main(args=[])',
+        forbidden_modules=forbidden_modules,
+        max_nonstdlib_module_count=max_nonstdlib_module_count,
+    )
diff -pruN 3.24.0-1/ase/test/cli/test_info.py 3.26.0-1/ase/test/cli/test_info.py
--- 3.24.0-1/ase/test/cli/test_info.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cli/test_info.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/cli/test_run.py 3.26.0-1/ase/test/cli/test_run.py
--- 3.24.0-1/ase/test/cli/test_run.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/cli/test_run.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/conftest.py 3.26.0-1/ase/test/conftest.py
--- 3.24.0-1/ase/test/conftest.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/conftest.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 import shutil
 import tempfile
diff -pruN 3.24.0-1/ase/test/constraints/test_CO2linear_Au111.py 3.26.0-1/ase/test/constraints/test_CO2linear_Au111.py
--- 3.24.0-1/ase/test/constraints/test_CO2linear_Au111.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_CO2linear_Au111.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import cos, pi, sin
 
 import pytest
diff -pruN 3.24.0-1/ase/test/constraints/test_external_force.py 3.26.0-1/ase/test/constraints/test_external_force.py
--- 3.24.0-1/ase/test/constraints/test_external_force.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_external_force.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 from numpy.linalg import norm
 
diff -pruN 3.24.0-1/ase/test/constraints/test_fix_bond_length_mic.py 3.26.0-1/ase/test/constraints/test_fix_bond_length_mic.py
--- 3.24.0-1/ase/test/constraints/test_fix_bond_length_mic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fix_bond_length_mic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 import ase
diff -pruN 3.24.0-1/ase/test/constraints/test_fix_symmetry.py 3.26.0-1/ase/test/constraints/test_fix_symmetry.py
--- 3.24.0-1/ase/test/constraints/test_fix_symmetry.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fix_symmetry.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/constraints/test_fixatoms.py 3.26.0-1/ase/test/constraints/test_fixatoms.py
--- 3.24.0-1/ase/test/constraints/test_fixatoms.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fixatoms.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.constraints import FixAtoms
 
diff -pruN 3.24.0-1/ase/test/constraints/test_fixbondlength_CO2_Au111.py 3.26.0-1/ase/test/constraints/test_fixbondlength_CO2_Au111.py
--- 3.24.0-1/ase/test/constraints/test_fixbondlength_CO2_Au111.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fixbondlength_CO2_Au111.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import cos, pi, sin
 
 import pytest
diff -pruN 3.24.0-1/ase/test/constraints/test_fixbonds.py 3.26.0-1/ase/test/constraints/test_fixbonds.py
--- 3.24.0-1/ase/test/constraints/test_fixbonds.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fixbonds.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.constraints import FixBondLengths
 
diff -pruN 3.24.0-1/ase/test/constraints/test_fixcartesian.py 3.26.0-1/ase/test/constraints/test_fixcartesian.py
--- 3.24.0-1/ase/test/constraints/test_fixcartesian.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fixcartesian.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
@@ -45,7 +46,7 @@ def test_fixcartesian_adjust(atoms):
 
     assert newpos == pytest.approx(newpos_expected, abs=1e-14)
 
-    oldforces = 1.0 + np.random.rand(len(atoms), 3)
+    oldforces = 1.0 + rng.random((len(atoms), 3))
     newforces = oldforces.copy()
     constraint.adjust_forces(atoms, newforces)
 
diff -pruN 3.24.0-1/ase/test/constraints/test_fixcom.py 3.26.0-1/ase/test/constraints/test_fixcom.py
--- 3.24.0-1/ase/test/constraints/test_fixcom.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fixcom.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for FixCom."""
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/constraints/test_fixedline_fixedplane.py 3.26.0-1/ase/test/constraints/test_fixedline_fixedplane.py
--- 3.24.0-1/ase/test/constraints/test_fixedline_fixedplane.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fixedline_fixedplane.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/constraints/test_fixedmode.py 3.26.0-1/ase/test/constraints/test_fixedmode.py
--- 3.24.0-1/ase/test/constraints/test_fixedmode.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fixedmode.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/constraints/test_fixinternals.py 3.26.0-1/ase/test/constraints/test_fixinternals.py
--- 3.24.0-1/ase/test/constraints/test_fixinternals.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fixinternals.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import copy
 
 import pytest
diff -pruN 3.24.0-1/ase/test/constraints/test_fixscaled.py 3.26.0-1/ase/test/constraints/test_fixscaled.py
--- 3.24.0-1/ase/test/constraints/test_fixscaled.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fixscaled.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/constraints/test_fixsubsetcom.py 3.26.0-1/ase/test/constraints/test_fixsubsetcom.py
--- 3.24.0-1/ase/test/constraints/test_fixsubsetcom.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_fixsubsetcom.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for FixCom."""
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/constraints/test_getindices.py 3.26.0-1/ase/test/constraints/test_getindices.py
--- 3.24.0-1/ase/test/constraints/test_getindices.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_getindices.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import fcc111
 from ase.constraints import (
     FixAtoms,
diff -pruN 3.24.0-1/ase/test/constraints/test_hookean.py 3.26.0-1/ase/test/constraints/test_hookean.py
--- 3.24.0-1/ase/test/constraints/test_hookean.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_hookean.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atom, Atoms, units
diff -pruN 3.24.0-1/ase/test/constraints/test_hookean_pbc.py 3.26.0-1/ase/test/constraints/test_hookean_pbc.py
--- 3.24.0-1/ase/test/constraints/test_hookean_pbc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_hookean_pbc.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.emt import EMT
 from ase.constraints import Hookean
diff -pruN 3.24.0-1/ase/test/constraints/test_mirror.py 3.26.0-1/ase/test/constraints/test_mirror.py
--- 3.24.0-1/ase/test/constraints/test_mirror.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_mirror.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/constraints/test_negativeindex.py 3.26.0-1/ase/test/constraints/test_negativeindex.py
--- 3.24.0-1/ase/test/constraints/test_negativeindex.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_negativeindex.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.atoms import Atoms
 from ase.constraints import FixScaled
 
diff -pruN 3.24.0-1/ase/test/constraints/test_parameteric_constr.py 3.26.0-1/ase/test/constraints/test_parameteric_constr.py
--- 3.24.0-1/ase/test/constraints/test_parameteric_constr.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_parameteric_constr.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/constraints/test_repeat_FixAtoms.py 3.26.0-1/ase/test/constraints/test_repeat_FixAtoms.py
--- 3.24.0-1/ase/test/constraints/test_repeat_FixAtoms.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_repeat_FixAtoms.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import molecule
 from ase.constraints import FixAtoms
 
diff -pruN 3.24.0-1/ase/test/constraints/test_setpos.py 3.26.0-1/ase/test/constraints/test_setpos.py
--- 3.24.0-1/ase/test/constraints/test_setpos.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/constraints/test_setpos.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/db/conftest.py 3.26.0-1/ase/test/db/conftest.py
--- 3.24.0-1/ase/test/db/conftest.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/conftest.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,86 +0,0 @@
-import os
-
-import pytest
-
-from ase.db import connect
-
-
-@pytest.fixture(scope='session')
-def mysql_port():
-    return int(os.environ.get('MYSQL_TCP_PORT', 3306))
-
-
-@pytest.fixture()
-def get_db_name(mysql_port):
-    """ Fixture that returns a function to get the test db name
-    for the different supported db types.
-
-    Args:
-        dbtype (str): Type of database. Currently only 5 types supported:
-            postgresql, mysql, mariadb, json, and db (sqlite3)
-        clean_db (bool): Whether to clean all entries from the db. Useful
-            for reusing the database across multiple tests. Defaults to True.
-    """
-    def _func(dbtype, clean_db=True):
-        name = None
-
-        if dbtype == 'postgresql':
-            pytest.importorskip('psycopg2')
-            if os.environ.get('POSTGRES_DB'):  # gitlab-ci
-                name = 'postgresql://ase:ase@postgres:5432/testase'
-            else:
-                name = os.environ.get('ASE_TEST_POSTGRES_URL')
-        elif dbtype == 'mysql':
-            pytest.importorskip('pymysql')
-            if os.environ.get('CI_PROJECT_DIR'):  # gitlab-ci
-                # Note: testing of non-standard port by changing from default
-                # of 3306 to 3307
-                name = f'mysql://root:ase@mysql:{mysql_port}/testase_mysql'
-            else:
-                name = os.environ.get('MYSQL_DB_URL')
-        elif dbtype == 'mariadb':
-            pytest.importorskip('pymysql')
-            if os.environ.get('CI_PROJECT_DIR'):  # gitlab-ci
-                # Note: testing of non-standard port by changing from default
-                # of 3306 to 3307
-                name = f'mariadb://root:ase@mariadb:{mysql_port}/testase_mysql'
-            else:
-                name = os.environ.get('MYSQL_DB_URL')
-        elif dbtype == 'json':
-            name = 'testase.json'
-        elif dbtype == 'db':
-            name = 'testase.db'
-        else:
-            raise ValueError(f'Bad db type: {dbtype}')
-
-        if name is None:
-            pytest.skip('Test requires environment variables')
-
-        if clean_db:
-            if dbtype in ["postgresql", "mysql", "mariadb"]:
-                c = connect(name)
-                c.delete([row.id for row in c.select()])
-
-        return name
-
-    return _func
-
-
-# For different parametrizations (we will move the heavier ones out anyway)
-dbtypes = ['db', 'postgresql', 'mysql', 'mariadb']
-
-
-@pytest.fixture(params=dbtypes)
-def dbtype(request):
-    return xfail_bad_backends(request)
-
-
-@pytest.fixture(params=['json', *dbtypes])
-def dbtype2(request):
-    return xfail_bad_backends(request)
-
-
-def xfail_bad_backends(request):
-    if request.param in {'postgresql', 'mysql', 'mariadb'}:
-        pytest.xfail(reason='race condition')
-    return request.param
diff -pruN 3.24.0-1/ase/test/db/test_cli.py 3.26.0-1/ase/test/db/test_cli.py
--- 3.24.0-1/ase/test/db/test_cli.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_cli.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Testing of "ase db" command-line interface."""
 from pathlib import Path
 
diff -pruN 3.24.0-1/ase/test/db/test_db.py 3.26.0-1/ase/test/db/test_db.py
--- 3.24.0-1/ase/test/db/test_db.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_db.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.db import connect
@@ -13,17 +14,17 @@ ase -T db -v testase.json natoms=1,Cu=1
 ase -T db -v testase.json "H>0" -k hydro=1,abc=42,foo=bar &&
 ase -T db -v testase.json "H>0" --delete-keys foo"""
 
-dbtypes = ['json', 'db', 'postgresql', 'mysql', 'mariadb']
+dbtypes = ['json', 'db']
 
 
 @pytest.mark.slow()
 @pytest.mark.parametrize('dbtype', dbtypes)
-def test_db(dbtype, cli, testdir, get_db_name):
+def test_db(dbtype, cli, testdir):
     def count(n, *args, **kwargs):
         m = len(list(con.select(columns=['id'], *args, **kwargs)))
         assert m == n, (m, n)
 
-    name = get_db_name(dbtype)
+    name = f'testase.{dbtype}'
 
     cli.shell(cmd.replace('testase.json', name))
 
diff -pruN 3.24.0-1/ase/test/db/test_db2.py 3.26.0-1/ase/test/db/test_db2.py
--- 3.24.0-1/ase/test/db/test_db2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_db2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
@@ -9,9 +10,9 @@ from ase.db import connect
 from ase.io import read
 
 
-def test_db2(testdir, dbtype2, get_db_name):
-    dbtype = dbtype2
-    name = get_db_name(dbtype)
+@pytest.mark.parametrize('dbtype', ['json', 'db'])
+def test_db2(testdir, dbtype):
+    name = f'testase.{dbtype}'
 
     c = connect(name)
     print(name, c)
diff -pruN 3.24.0-1/ase/test/db/test_db_web.py 3.26.0-1/ase/test/db/test_db_web.py
--- 3.24.0-1/ase/test/db/test_db_web.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_db_web.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import io
 
 import pytest
diff -pruN 3.24.0-1/ase/test/db/test_jsondb.py 3.26.0-1/ase/test/db/test_jsondb.py
--- 3.24.0-1/ase/test/db/test_jsondb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_jsondb.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from io import StringIO
 
 from ase.io import read, write
diff -pruN 3.24.0-1/ase/test/db/test_metadata.py 3.26.0-1/ase/test/db/test_metadata.py
--- 3.24.0-1/ase/test/db/test_metadata.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_metadata.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/db/test_mysql.py 3.26.0-1/ase/test/db/test_mysql.py
--- 3.24.0-1/ase/test/db/test_mysql.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_mysql.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,117 +0,0 @@
-import os
-
-import pytest
-
-from ase import Atoms
-from ase.build import molecule
-from ase.calculators.emt import EMT
-from ase.db import connect
-
-
-@pytest.fixture(scope='module')
-def url(mysql_port):
-    pytest.importorskip('pymysql')
-
-    on_ci_server = 'CI_PROJECT_DIR' in os.environ
-
-    if on_ci_server:
-        # CI server configured to use non-standard port 3307
-        # instead of the default 3306 port. This is to test
-        # for proper passing of the port for creating the
-        # mysql connection
-        db_url = f'mysql://root:ase@mysql:{mysql_port}/testase_mysql'
-        # HOST = 'mysql'
-        # USER = 'root'
-        # PASSWD = 'ase'
-        # DB_NAME = 'testase_mysql'
-    else:
-        db_url = os.environ.get('MYSQL_DB_URL')
-        # HOST = os.environ.get('MYSQL_HOST', None)
-        # USER = os.environ.get('MYSQL_USER', None)
-        # PASSWD = os.environ.get('MYSQL_PASSWD', None)
-        # DB_NAME = os.environ.get('MYSQL_DB_NAME', None)
-
-    if db_url is None:
-        msg = ('Not on GitLab CI server. To run this test '
-               'host, username, password and database name '
-               'must be in the environment variables '
-               'MYSQL_HOST, MYSQL_USER, MYSQL_PASSWD and '
-               'MYSQL_DB_NAME, respectively.')
-        pytest.skip(msg)
-    return db_url
-
-
-@pytest.fixture()
-def db(url):
-    return connect(url)
-
-
-@pytest.fixture()
-def h2o():
-    return molecule('H2O')
-
-
-def test_connect(db):
-    db.delete([row.id for row in db.select()])
-
-
-def test_write_read(db):
-    co = Atoms('CO', positions=[(0, 0, 0), (0, 0, 1.1)])
-    uid = db.write(co, tag=1, type='molecule')
-
-    co_db = db.get(id=uid)
-    atoms_db = co_db.toatoms()
-
-    assert len(atoms_db) == 2
-    assert atoms_db[0].symbol == co[0].symbol
-    assert atoms_db[1].symbol == co[1].symbol
-    assert co_db.tag == 1
-    assert co_db.type == 'molecule'
-
-
-def test_write_read_with_calculator(db, h2o):
-    calc = EMT(dummy_param=2.4)
-    h2o.calc = calc
-
-    uid = db.write(h2o)
-
-    h2o_db = db.get(id=uid).toatoms()
-
-    # Back in the days we allowed reconstructing calculators.
-    # For security we don't anymore.
-    assert h2o_db.calc is None
-
-    # Check that get_atoms function works
-    db.get_atoms(H=2)
-    # XXX We should assert something should we not?
-
-
-def test_update(db, h2o):
-    h2o = molecule('H2O')
-
-    uid = db.write(h2o, type='molecule')
-    db.update(id=uid, type='oxide')
-
-    atoms_type = db.get(id=uid).type
-
-    assert atoms_type == 'oxide'
-
-
-def test_delete(db, h2o):
-    h2o = molecule('H2O')
-    uid = db.write(h2o, type='molecule')
-
-    # Make sure that we can get the value
-    db.get(id=uid)
-    db.delete([uid])
-
-    with pytest.raises(KeyError):
-        db.get(id=uid)
-
-
-def test_read_write_bool_key_value_pair(db, h2o):
-    # Make sure we can read and write boolean key value pairs
-    uid = db.write(h2o, is_water=True, is_solid=False)
-    row = db.get(id=uid)
-    assert row.is_water
-    assert not row.is_solid
diff -pruN 3.24.0-1/ase/test/db/test_o2b2o.py 3.26.0-1/ase/test/db/test_o2b2o.py
--- 3.24.0-1/ase/test/db/test_o2b2o.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_o2b2o.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,5 +1,4 @@
-import pickle
-
+# fmt: off
 import numpy as np
 
 from ase.cell import Cell
@@ -13,11 +12,7 @@ def test_o2b2o():
                ['a', 42, True, None, np.nan, np.inf, 1j],
                Cell(np.eye(3)),
                {'a': {'b': {'c': np.ones(3)}}}]:
-        p1 = pickle.dumps(o1)
         b1 = object_to_bytes(o1)
         o2 = bytes_to_object(b1)
-        p2 = pickle.dumps(o2)
         print(o2)
-        print(b1)
-        print()
-        assert p1 == p2, (o1, p1, p2, vars(o1), vars(p1), vars(p2))
+        assert repr(o1) == repr(o2)
diff -pruN 3.24.0-1/ase/test/db/test_row.py 3.26.0-1/ase/test/db/test_row.py
--- 3.24.0-1/ase/test/db/test_row.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_row.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.db.row import AtomsRow
 
diff -pruN 3.24.0-1/ase/test/db/test_sql_db_ext_tables.py 3.26.0-1/ase/test/db/test_sql_db_ext_tables.py
--- 3.24.0-1/ase/test/db/test_sql_db_ext_tables.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_sql_db_ext_tables.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
@@ -6,12 +7,11 @@ from ase.db import connect
 from ase.db.sqlite import all_tables
 
 
-def test_create_and_delete_ext_tab(testdir, get_db_name, dbtype):
+def test_create_and_delete_ext_tab(testdir):
     ext_tab = ["tab1", "tab2", "tab3"]
     atoms = Atoms()
 
-    name = get_db_name(dbtype)
-    db = connect(name)
+    db = connect('test.db')
     db.write(atoms)
 
     for tab in ext_tab:
@@ -24,11 +24,10 @@ def test_create_and_delete_ext_tab(testd
     assert "tab1" not in db._get_external_table_names()
 
 
-def test_insert_in_external_tables(testdir, get_db_name, dbtype):
+def test_insert_in_external_tables(testdir):
     atoms = Atoms()
 
-    name = get_db_name(dbtype)
-    db = connect(name)
+    db = connect('test.db')
 
     # Now a table called insert_tab with schema datatype REAL should
     # be created
@@ -118,11 +117,10 @@ def test_insert_in_external_tables(testd
             db.write(atoms, external_tables={tab_name: {"value": 1}})
 
 
-def test_extract_from_table(testdir, get_db_name, dbtype):
+def test_extract_from_table(testdir):
     atoms = Atoms()
 
-    name = get_db_name(dbtype)
-    db = connect(name)
+    db = connect('test.db')
     uid = db.write(
         atoms,
         external_tables={
@@ -136,11 +134,10 @@ def test_extract_from_table(testdir, get
     assert abs(row["insert_tab"]["rate1"] + 10.0) < 1E-8
 
 
-def test_write_atoms_row(testdir, get_db_name, dbtype):
+def test_write_atoms_row(testdir):
     atoms = Atoms()
 
-    name = get_db_name(dbtype)
-    db = connect(name)
+    db = connect('test.db')
     uid = db.write(
         atoms, external_tables={
             "insert_tab": {"rate": 12.0, "rate1": -10.0},
@@ -152,9 +149,8 @@ def test_write_atoms_row(testdir, get_db
     db.write(row)
 
 
-def test_external_table_upon_update(testdir, get_db_name, dbtype):
-    name = get_db_name(dbtype)
-    db = connect(name)
+def test_external_table_upon_update(testdir):
+    db = connect('test.db')
     no_features = 500
     ext_table = {i: i for i in range(no_features)}
     atoms = Atoms('Pb', positions=[[0, 0, 0]])
@@ -162,9 +158,8 @@ def test_external_table_upon_update(test
     db.update(uid, external_tables={'sys': ext_table})
 
 
-def test_external_table_upon_update_with_float(testdir, get_db_name, dbtype):
-    name = get_db_name(dbtype)
-    db = connect(name)
+def test_external_table_upon_update_with_float(testdir):
+    db = connect('test.db')
     ext_table = {'value1': 1.0, 'value2': 2.0}
     atoms = Atoms('Pb', positions=[[0, 0, 0]])
     uid = db.write(atoms)
diff -pruN 3.24.0-1/ase/test/db/test_sqlite.py 3.26.0-1/ase/test/db/test_sqlite.py
--- 3.24.0-1/ase/test/db/test_sqlite.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_sqlite.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import pytest
diff -pruN 3.24.0-1/ase/test/db/test_table.py 3.26.0-1/ase/test/db/test_table.py
--- 3.24.0-1/ase/test/db/test_table.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_table.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from types import SimpleNamespace
 
 from ase.db.table import Table
diff -pruN 3.24.0-1/ase/test/db/test_update.py 3.26.0-1/ase/test/db/test_update.py
--- 3.24.0-1/ase/test/db/test_update.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/db/test_update.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from time import time
 
 import pytest
diff -pruN 3.24.0-1/ase/test/dft/test_bandgap.py 3.26.0-1/ase/test/dft/test_bandgap.py
--- 3.24.0-1/ase/test/dft/test_bandgap.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/dft/test_bandgap.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.dft.bandgap import bandgap
diff -pruN 3.24.0-1/ase/test/dft/test_bee.py 3.26.0-1/ase/test/dft/test_bee.py
--- 3.24.0-1/ase/test/dft/test_bee.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/dft/test_bee.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/dft/test_bz.py 3.26.0-1/ase/test/dft/test_bz.py
--- 3.24.0-1/ase/test/dft/test_bz.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/dft/test_bz.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import matplotlib.pyplot as plt
 import numpy as np
 from matplotlib.testing.compare import compare_images
diff -pruN 3.24.0-1/ase/test/dft/test_dos.py 3.26.0-1/ase/test/dft/test_dos.py
--- 3.24.0-1/ase/test/dft/test_dos.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/dft/test_dos.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.atoms import Atoms
 from ase.calculators.singlepoint import (
     SinglePointDFTCalculator,
diff -pruN 3.24.0-1/ase/test/dft/test_hex.py 3.26.0-1/ase/test/dft/test_hex.py
--- 3.24.0-1/ase/test/dft/test_hex.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/dft/test_hex.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/dft/test_interpolate.py 3.26.0-1/ase/test/dft/test_interpolate.py
--- 3.24.0-1/ase/test/dft/test_interpolate.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/dft/test_interpolate.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.dft.kpoints import monkhorst_pack_interpolate
diff -pruN 3.24.0-1/ase/test/dft/test_kpt_density_monkhorst_pack.py 3.26.0-1/ase/test/dft/test_kpt_density_monkhorst_pack.py
--- 3.24.0-1/ase/test/dft/test_kpt_density_monkhorst_pack.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/dft/test_kpt_density_monkhorst_pack.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/dft/test_kpts_size_offsets.py 3.26.0-1/ase/test/dft/test_kpts_size_offsets.py
--- 3.24.0-1/ase/test/dft/test_kpts_size_offsets.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/dft/test_kpts_size_offsets.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/dft/test_min_distance_monkhorst_pack.py 3.26.0-1/ase/test/dft/test_min_distance_monkhorst_pack.py
--- 3.24.0-1/ase/test/dft/test_min_distance_monkhorst_pack.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/dft/test_min_distance_monkhorst_pack.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/dft/test_monoclinic.py 3.26.0-1/ase/test/dft/test_monoclinic.py
--- 3.24.0-1/ase/test/dft/test_monoclinic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/dft/test_monoclinic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.test import FreeElectrons
 from ase.cell import Cell
diff -pruN 3.24.0-1/ase/test/emt/test_emt.py 3.26.0-1/ase/test/emt/test_emt.py
--- 3.24.0-1/ase/test/emt/test_emt.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/emt/test_emt.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/emt/test_emt1.py 3.26.0-1/ase/test/emt/test_emt1.py
--- 3.24.0-1/ase/test/emt/test_emt1.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/emt/test_emt1.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.emt import EMT
 from ase.constraints import FixBondLength
diff -pruN 3.24.0-1/ase/test/emt/test_emt2.py 3.26.0-1/ase/test/emt/test_emt2.py
--- 3.24.0-1/ase/test/emt/test_emt2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/emt/test_emt2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.build import molecule
 from ase.calculators.emt import EMT
diff -pruN 3.24.0-1/ase/test/emt/test_emt_h3o2m.py 3.26.0-1/ase/test/emt/test_emt_h3o2m.py
--- 3.24.0-1/ase/test/emt/test_emt_h3o2m.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/emt/test_emt_h3o2m.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import cos, radians, sin
 
 import pytest
diff -pruN 3.24.0-1/ase/test/emt/test_emt_stress.py 3.26.0-1/ase/test/emt/test_emt_stress.py
--- 3.24.0-1/ase/test/emt/test_emt_stress.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/emt/test_emt_stress.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/factories.py 3.26.0-1/ase/test/factories.py
--- 3.24.0-1/ase/test/factories.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/factories.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,7 +1,9 @@
+# fmt: off
 import importlib.util
 import os
 import re
 import tempfile
+from functools import cached_property
 from pathlib import Path
 
 import pytest
@@ -14,7 +16,7 @@ from ase.calculators.castep import Caste
 from ase.calculators.cp2k import CP2K, Cp2kShell
 from ase.calculators.dftb import Dftb
 from ase.calculators.dftd3 import DFTD3
-from ase.calculators.elk import ELK
+from ase.calculators.elk import ELK, ElkTemplate
 from ase.calculators.espresso import Espresso, EspressoTemplate
 from ase.calculators.exciting.exciting import (
     ExcitingGroundStateCalculator,
@@ -30,7 +32,6 @@ from ase.calculators.siesta import Siest
 from ase.calculators.vasp import Vasp, get_vasp_version
 from ase.config import Config
 from ase.io.espresso import Namelist
-from ase.utils import lazyproperty
 
 
 class NotInstalled(Exception):
@@ -45,7 +46,7 @@ Could not import asetest package.  Pleas
 using e.g. "pip install ase-datafiles" to run calculator integration
 tests.""")
 
-    @lazyproperty
+    @cached_property
     def datafiles_module(self):
         try:
             import asetest
@@ -53,7 +54,7 @@ tests.""")
             return None
         return asetest
 
-    @lazyproperty
+    @cached_property
     def datafile_config(self):
         # XXXX TODO avoid requiring the dummy [parallel] section
         datafiles = self.datafiles_module
@@ -90,7 +91,7 @@ pseudo_path = {path}/siesta
 """
         return datafile_config
 
-    @lazyproperty
+    @cached_property
     def cfg(self):
         # First we load the usual configfile.
         # But we don't want to run tests against the user's production
@@ -249,13 +250,11 @@ class DFTD3Factory:
 @factory('elk')
 class ElkFactory:
     def __init__(self, cfg):
-        self.profile = ELK.load_argv_profile(cfg, 'elk')
+        self.profile = ElkTemplate().load_profile(cfg)
         self.species_dir = cfg.parser['elk']['species_dir']
 
     def version(self):
-        output = read_stdout(self.profile._split_command)
-        match = re.search(r'Elk code version (\S+)', output, re.M)
-        return match.group(1)
+        return self.profile.version()
 
     def calc(self, **kwargs):
         return ELK(profile=self.profile, species_dir=self.species_dir, **kwargs)
@@ -269,7 +268,7 @@ class EspressoFactory:
     def version(self):
         return self.profile.version()
 
-    @lazyproperty
+    @cached_property
     def pseudopotentials(self):
         pseudopotentials = {}
         for path in Path(self.profile.pseudo_dir).glob('*.UPF'):
diff -pruN 3.24.0-1/ase/test/filter/test_cellfilter.py 3.26.0-1/ase/test/filter/test_cellfilter.py
--- 3.24.0-1/ase/test/filter/test_cellfilter.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/filter/test_cellfilter.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,9 +1,11 @@
+# fmt: off
 from itertools import product
 
 import numpy as np
 import pytest
 
 import ase
+from ase import Atoms
 from ase.build import bulk
 from ase.calculators.test import gradient_test
 from ase.filters import ExpCellFilter, Filter, FrechetCellFilter, UnitCellFilter
@@ -27,8 +29,8 @@ def atoms(asap3) -> ase.Atoms:
 @pytest.mark.parametrize(
     'cellfilter', [UnitCellFilter, FrechetCellFilter, ExpCellFilter]
 )
-def test_get_and_set_positions(atoms, cellfilter):
-    filter: Filter = cellfilter(atoms)
+def test_get_and_set_positions(atoms: Atoms, cellfilter: type[Filter]) -> None:
+    filter = cellfilter(atoms)
     pos = filter.get_positions()
     filter.set_positions(pos)
     pos2 = filter.get_positions()
@@ -86,8 +88,8 @@ def test_cellfilter_stress(
     # Check gradient at other than origin
     natoms = len(atoms)
     pos0 = filter.get_positions()
-    np.random.seed(0)
-    pos0[natoms:, :] += 1e-2 * np.random.randn(3, 3)
+    rng = np.random.RandomState(0)
+    pos0[natoms:, :] += 1e-2 * rng.randn(3, 3)
     filter.set_positions(pos0)
     grads_actual = -filter.get_forces()
 
diff -pruN 3.24.0-1/ase/test/filter/test_filter.py 3.26.0-1/ase/test/filter/test_filter.py
--- 3.24.0-1/ase/test/filter/test_filter.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/filter/test_filter.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,9 +1,18 @@
+# fmt: off
+import numpy as np
 import pytest
 
-from ase.build import molecule
+from ase.build import bulk, molecule
 from ase.calculators.emt import EMT
-from ase.filters import Filter
+from ase.filters import (
+    ExpCellFilter,
+    Filter,
+    FrechetCellFilter,
+    StrainFilter,
+    UnitCellFilter,
+)
 from ase.optimize import QuasiNewton
+from ase.stress import full_3x3_to_voigt_6_strain, voigt_6_to_full_3x3_strain
 
 
 @pytest.mark.optimize()
@@ -18,3 +27,41 @@ def test_filter(testdir):
                      logfile='filter-test.log') as opt:
         opt.run()
     # No assertions=??
+
+
+@pytest.mark.optimize()
+@pytest.mark.filterwarnings("ignore:Use FrechetCellFilter")
+@pytest.mark.parametrize(
+    'filterclass', [StrainFilter,
+                    UnitCellFilter,
+                    FrechetCellFilter,
+                    ExpCellFilter])
+@pytest.mark.parametrize(
+    'mask', [[1, 1, 0, 0, 0, 1],
+             [1, 0, 0, 0, 0, 0],
+             [1, 0, 1, 1, 0, 0],
+             [0, 1, 0, 0, 1, 1]]
+)
+def test_apply_strain_to_mask(filterclass, mask):
+    cu = bulk('Cu', a=3.14) * (6, 3, 1)
+    orig_cell = cu.cell.copy()
+    rng = np.random.RandomState(69)
+
+    # Create extreme deformation
+    deformation_vv = np.eye(3) + 1e2 * rng.randn(3, 3)
+    filter = filterclass(cu, mask=mask)
+    if filterclass is not StrainFilter:
+        pos_and_deform = \
+            np.concatenate((cu.get_positions(),
+                            deformation_vv), axis=0)
+    else:
+        pos_and_deform = full_3x3_to_voigt_6_strain(deformation_vv)
+
+    # Apply the deformation to the filter, which should then apply it
+    # with a mask.
+    filter.set_positions(pos_and_deform)
+    full_mask = voigt_6_to_full_3x3_strain(mask) != 0
+
+    # Ensure the mask is respected to a very tight numerical tolerance
+    assert np.linalg.solve(orig_cell, cu.cell)[~full_mask] == \
+        pytest.approx(np.eye(3)[~full_mask], abs=1e-12)
diff -pruN 3.24.0-1/ase/test/filter/test_strain.py 3.26.0-1/ase/test/filter/test_strain.py
--- 3.24.0-1/ase/test/filter/test_strain.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/filter/test_strain.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import sqrt
 
 import pytest
diff -pruN 3.24.0-1/ase/test/filter/test_strain_emt.py 3.26.0-1/ase/test/filter/test_strain_emt.py
--- 3.24.0-1/ase/test/filter/test_strain_emt.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/filter/test_strain_emt.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/fio/aims/test_aims_out.py 3.26.0-1/ase/test/fio/aims/test_aims_out.py
--- 3.24.0-1/ase/test/fio/aims/test_aims_out.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/aims/test_aims_out.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # flake8: noqa
 from pathlib import Path
 
diff -pruN 3.24.0-1/ase/test/fio/aims/test_aims_out_chunks.py 3.26.0-1/ase/test/fio/aims/test_aims_out_chunks.py
--- 3.24.0-1/ase/test/fio/aims/test_aims_out_chunks.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/aims/test_aims_out_chunks.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # flake8: noqa
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/fio/aims/test_geometry.py 3.26.0-1/ase/test/fio/aims/test_geometry.py
--- 3.24.0-1/ase/test/fio/aims/test_geometry.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/aims/test_geometry.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import warnings
 
 import numpy as np
@@ -104,15 +105,11 @@ def test_wrap_Si(Si):
     """write fractional coords and check if structure was preserved"""
     Si.positions[0, 0] -= 0.015625
     Si.write(file, format=format, scaled=True, wrap=True)
-
     new_atoms = read(file)
 
-    try:
-        assert np.allclose(Si.positions, new_atoms.positions)
-        raise ValueError("Wrapped atoms not passed to new geometry.in file")
-    except AssertionError:
-        Si.wrap()
-        assert np.allclose(Si.positions, new_atoms.positions)
+    assert not np.allclose(Si.positions, new_atoms.positions)
+    Si.wrap()
+    assert np.allclose(Si.positions, new_atoms.positions)
 
 
 def test_constraints_Si(Si):
diff -pruN 3.24.0-1/ase/test/fio/aims/test_write_control.py 3.26.0-1/ase/test/fio/aims/test_write_control.py
--- 3.24.0-1/ase/test/fio/aims/test_write_control.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/aims/test_write_control.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test writing control.in files for Aims using ase.io.aims.
 
 Control.in file contains calculation parameters such as the functional and
diff -pruN 3.24.0-1/ase/test/fio/castep/test_castep_reader.py 3.26.0-1/ase/test/fio/castep/test_castep_reader.py
--- 3.24.0-1/ase/test/fio/castep/test_castep_reader.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/castep/test_castep_reader.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for the Castep.read method"""
 from io import StringIO
 
@@ -207,6 +208,85 @@ HEADER_DETAILED = """\
 """  # noqa: E501
 
 
+# Some XC functionals cannot be mapped by ASE to keywords; e.g. from Castep 25
+HEADER_PZ_LDA = """\
+ ************************************ Title ************************************
+ 
+
+ ***************************** General Parameters ******************************
+  
+ output verbosity                               : normal  (1)
+ write checkpoint data to                       : castep.check
+ type of calculation                            : single point energy
+ stress calculation                             : on
+ density difference calculation                 : off
+ electron localisation func (ELF) calculation   : off
+ Hirshfeld analysis                             : off
+ polarisation (Berry phase) analysis            : off
+ molecular orbital projected DOS                : off
+ deltaSCF calculation                           : off
+ unlimited duration calculation
+ timing information                             : on
+ memory usage estimate                          : on
+ write extra output files                       : on
+ write final potential to formatted file        : off
+ write final density to formatted file          : off
+ write BibTeX reference list                    : on
+ write OTFG pseudopotential files               : on
+ write electrostatic potential file             : on
+ write bands file                               : on
+ checkpoint writing                             : both castep_bin and check files
+ random number generator seed                   :  112211524
+
+ *********************** Exchange-Correlation Parameters ***********************
+  
+ using functional                               : Perdew-Zunger Local Density Approximation
+ DFT+D: Semi-empirical dispersion correction    : off
+
+ ************************* Pseudopotential Parameters **************************
+  
+ pseudopotential representation                 : reciprocal space
+ <beta|phi> representation                      : reciprocal space
+ spin-orbit coupling                            : off
+
+ **************************** Basis Set Parameters *****************************
+  
+ basis set accuracy                             : FINE
+ finite basis set correction                    : none
+
+ **************************** Electronic Parameters ****************************
+  
+ number of  electrons                           :  28.00    
+ net charge of system                           :  0.000    
+ treating system as non-spin-polarized
+ number of bands                                :         18
+
+ ********************* Electronic Minimization Parameters **********************
+  
+ Method: Treating system as metallic with density mixing treatment of electrons,
+         and number of  SD  steps               :          1
+         and number of  CG  steps               :          4
+  
+ total energy / atom convergence tol.           : 0.1000E-04   eV
+ eigen-energy convergence tolerance             : 0.1429E-06   eV
+ max force / atom convergence tol.              : ignored
+ periodic dipole correction                     : NONE
+
+ ************************** Density Mixing Parameters **************************
+  
+ density-mixing scheme                          : Pulay
+ max. length of mixing history                  :         20
+
+ *********************** Population Analysis Parameters ************************
+  
+ Population analysis with cutoff                :  3.000       A
+ Population analysis output                     : summary and pdos components
+
+ *******************************************************************************
+
+"""  # noqa: E501,W291,W293
+
+
 def test_header():
     """Test if the header blocks can be parsed correctly."""
     out = StringIO(HEADER)
@@ -244,6 +324,24 @@ def test_header_detailed():
     }
     assert parameters == parameters_ref
 
+
+def test_header_castep25():
+    """Test if header block with unknown XC functional is parsed correctly"""
+    out = StringIO(HEADER_PZ_LDA)
+    parameters = _read_header(out)
+    parameters_ref = {
+        "task": "SinglePoint",
+        "iprint": 1,
+        "calculate_stress": True,
+        "xc_functional": "Perdew-Zunger Local Density Approximation",
+        "sedc_apply": False,
+        "basis_precision": "FINE",
+        "finite_basis_corr": 0,
+        "elec_energy_tol": 1e-5,
+        "mixing_scheme": "Pulay",
+    }
+    assert parameters == parameters_ref
+
 
 UNIT_CELL = """\
                            -------------------------------
diff -pruN 3.24.0-1/ase/test/fio/castep/test_cell.py 3.26.0-1/ase/test/fio/castep/test_cell.py
--- 3.24.0-1/ase/test/fio/castep/test_cell.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/test/fio/castep/test_cell.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,203 @@
+# fmt: off
+"""Tests for CASTEP parsers"""
+import io
+import os
+import re
+import warnings
+
+import numpy as np
+import pytest
+
+from ase import Atoms
+from ase.build import molecule
+from ase.calculators.calculator import compare_atoms
+from ase.constraints import FixAtoms, FixCartesian, FixedLine, FixedPlane
+from ase.io import read, write
+from ase.io.castep import read_castep_cell, write_castep_cell
+
+
+# create mol with custom mass - from a list of positions or using
+# ase.build.molecule
+def write_read_atoms(atom, tmp_path):
+    write(os.path.join(tmp_path, "castep_test.cell"), atom)
+    return read(os.path.join(tmp_path, "castep_test.cell"))
+
+
+# write to .cell and check that .cell has correct species_mass block in it
+@pytest.mark.parametrize(
+    "mol, custom_masses, expected_species, expected_mass_block",
+    [
+        ("CH4", {2: [1]}, ["C", "H:0", "H", "H", "H"], ["H:0 2.0"]),
+        ("CH4", {2: [1, 2, 3, 4]}, ["C", "H", "H", "H", "H"], ["H 2.0"]),
+        ("C2H5", {2: [2, 3]}, ["C", "C", "H:0",
+         "H:0", "H", "H", "H"], ["H:0 2.0"]),
+        (
+            "C2H5",
+            {2: [2], 3: [3]},
+            ["C", "C", "H:0", "H:1", "H", "H", "H"],
+            ["H:0 2.0", "H:1 3.0"],
+        ),
+    ],
+)
+def test_custom_mass_write(
+    mol, custom_masses, expected_species, expected_mass_block, tmp_path
+):
+
+    custom_atoms = molecule(mol)
+    atom_positions = custom_atoms.positions
+
+    for mass, indices in custom_masses.items():
+        for i in indices:
+            custom_atoms[i].mass = mass
+
+    atom_masses = custom_atoms.get_masses()
+    # CASTEP IO can be noisy while handling keywords JSON
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", category=UserWarning)
+        new_atoms = write_read_atoms(custom_atoms, tmp_path)
+
+    # check atoms have been written and read correctly
+    np.testing.assert_allclose(atom_positions, new_atoms.positions)
+    np.testing.assert_allclose(atom_masses, new_atoms.get_masses())
+
+    # check that file contains appropriate blocks
+    with open(os.path.join(tmp_path, "castep_test.cell")) as f:
+        data = f.read().replace("\n", "\\n")
+
+    position_block = re.search(
+        r"%BLOCK POSITIONS_ABS.*%ENDBLOCK POSITIONS_ABS", data)
+    assert position_block
+
+    pos = position_block.group().split("\\n")[1:-1]
+    species = [p.split(" ")[0] for p in pos]
+    assert species == expected_species
+
+    mass_block = re.search(r"%BLOCK SPECIES_MASS.*%ENDBLOCK SPECIES_MASS", data)
+    assert mass_block
+
+    masses = mass_block.group().split("\\n")[1:-1]
+    for line, expected_line in zip(masses, expected_mass_block):
+        species_name, mass_read = line.split(' ')
+        expected_species_name, expected_mass = expected_line.split(' ')
+        assert pytest.approx(float(mass_read), abs=1e-6) == float(expected_mass)
+        assert species_name == expected_species_name
+
+
+# test setting a custom species on different atom before write
+def test_custom_mass_overwrite(tmp_path):
+    custom_atoms = molecule("CH4")
+    custom_atoms[1].mass = 2
+
+    # CASTEP IO is noisy while handling keywords JSON
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", category=UserWarning)
+        atoms = write_read_atoms(custom_atoms, tmp_path)
+
+    # test that changing masses when custom masses defined causes errors
+    atoms[3].mass = 3
+    with pytest.raises(ValueError,
+                       match="Could not write custom mass block for H."):
+        atoms.write(os.path.join(tmp_path, "castep_test2.cell"))
+
+
+# suppress UserWarning due to keyword_tolerance
+@pytest.mark.filterwarnings("ignore::UserWarning")
+class TestConstraints:
+    """Test if the constraint can be recovered when writing and reading.
+
+    Linear constraints in the CASTEP `.cell` format are flexible.
+    The present `read_castep_cell` converts the linear constraints into single
+    FixAtoms for the atoms for which all the three directions are fixed.
+    Otherwise, it makes either `FixedLine` or `FixPlane` depending on the
+    number of fixed directions for each atom.
+    """
+
+    # TODO: test also mask for FixCartesian
+
+    @staticmethod
+    def _make_atoms_ref():
+        """water molecule"""
+        atoms = molecule("H2O")
+        atoms.cell = 10.0 * np.eye(3)
+        atoms.pbc = True
+        atoms.set_initial_magnetic_moments(len(atoms) * [0.0])
+        return atoms
+
+    def _apply_write_read(self, constraint) -> Atoms:
+        atoms_ref = self._make_atoms_ref()
+        atoms_ref.set_constraint(constraint)
+
+        buf = io.StringIO()
+        write_castep_cell(buf, atoms_ref)
+        buf.seek(0)
+        atoms = read_castep_cell(buf)
+
+        assert not compare_atoms(atoms_ref, atoms)
+
+        print(atoms_ref.constraints, atoms.constraints)
+
+        return atoms
+
+    def test_fix_atoms(self):
+        """Test FixAtoms"""
+        constraint = FixAtoms(indices=(1, 2))
+        atoms = self._apply_write_read(constraint)
+
+        assert len(atoms.constraints) == 1
+        assert isinstance(atoms.constraints[0], FixAtoms)
+        assert all(atoms.constraints[0].index == constraint.index)
+
+    def test_fix_cartesian_line(self):
+        """Test FixCartesian along line"""
+        # moved only along the z direction
+        constraint = FixCartesian(0, mask=(1, 1, 0))
+        atoms = self._apply_write_read(constraint)
+
+        assert len(atoms.constraints) == 1
+        for i, idx in enumerate(constraint.index):
+            assert isinstance(atoms.constraints[i], FixedLine)
+            assert atoms.constraints[i].index.tolist() == [idx]
+
+    def test_fix_cartesian_plane(self):
+        """Test FixCartesian in plane"""
+        # moved only in the yz plane
+        constraint = FixCartesian((1, 2), mask=(1, 0, 0))
+        atoms = self._apply_write_read(constraint)
+
+        assert len(atoms.constraints) == 2
+        for i, idx in enumerate(constraint.index):
+            assert isinstance(atoms.constraints[i], FixedPlane)
+            assert atoms.constraints[i].index.tolist() == [idx]
+
+    def test_fix_cartesian_multiple(self):
+        """Test multiple FixCartesian"""
+        constraint = [FixCartesian(1), FixCartesian(2)]
+        atoms = self._apply_write_read(constraint)
+
+        assert len(atoms.constraints) == 1
+        assert isinstance(atoms.constraints[0], FixAtoms)
+        assert atoms.constraints[0].index.tolist() == [1, 2]
+
+    def test_fixed_line(self):
+        """Test FixedLine"""
+        # moved only along the z direction
+        constraint = FixedLine(0, direction=(0, 0, 1))
+        atoms = self._apply_write_read(constraint)
+
+        assert len(atoms.constraints) == 1
+        for i, idx in enumerate(constraint.index):
+            assert isinstance(atoms.constraints[i], FixedLine)
+            assert atoms.constraints[i].index.tolist() == [idx]
+            assert np.allclose(atoms.constraints[i].dir, constraint.dir)
+
+    def test_fixed_plane(self):
+        """Test FixedPlane"""
+        # moved only in the yz plane
+        constraint = FixedPlane((1, 2), direction=(1, 0, 0))
+        atoms = self._apply_write_read(constraint)
+
+        assert len(atoms.constraints) == 2
+        for i, idx in enumerate(constraint.index):
+            assert isinstance(atoms.constraints[i], FixedPlane)
+            assert atoms.constraints[i].index.tolist() == [idx]
+            assert np.allclose(atoms.constraints[i].dir, constraint.dir)
diff -pruN 3.24.0-1/ase/test/fio/castep/test_geom_md_ts.py 3.26.0-1/ase/test/fio/castep/test_geom_md_ts.py
--- 3.24.0-1/ase/test/fio/castep/test_geom_md_ts.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/test/fio/castep/test_geom_md_ts.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,66 @@
+# fmt: off
+"""Tests for parsers of CASTEP .geom, .md, .ts files"""
+import io
+
+import numpy as np
+import pytest
+
+from ase.build import bulk
+from ase.calculators.calculator import compare_atoms
+from ase.calculators.emt import EMT
+from ase.io.castep import (
+    read_castep_geom,
+    read_castep_md,
+    write_castep_geom,
+    write_castep_md,
+)
+
+
+@pytest.fixture(name='images_ref')
+def fixture_images():
+    """Fixture of reference images"""
+    atoms0 = bulk('Au', cubic=True)
+    atoms0.rattle(seed=42)
+    atoms1 = atoms0.copy()
+    atoms1.symbols[0] = 'Cu'
+    return [atoms0, atoms1]
+
+
+def test_write_and_read_geom(images_ref):
+    """Test if writing and reading .geom file get the original images back"""
+    for atoms in images_ref:
+        atoms.calc = EMT()
+        atoms.get_stress()
+    fd = io.StringIO()
+    write_castep_geom(fd, images_ref)
+    fd.seek(0)
+    images = read_castep_geom(fd, index=':')
+    assert len(images) == len(images_ref)
+    for atoms, atoms_ref in zip(images, images_ref):
+        assert not compare_atoms(atoms, atoms_ref)
+        for key in ['free_energy', 'forces', 'stress']:
+            np.testing.assert_allclose(
+                atoms.calc.results[key],
+                atoms_ref.calc.results[key],
+                err_msg=key,
+            )
+
+
+def test_write_and_read_md(images_ref):
+    """Test if writing and reading .md file get the original images back"""
+    for atoms in images_ref:
+        atoms.calc = EMT()
+        atoms.get_stress()
+    fd = io.StringIO()
+    write_castep_md(fd, images_ref)
+    fd.seek(0)
+    images = read_castep_md(fd, index=':')
+    assert len(images) == len(images_ref)
+    for atoms, atoms_ref in zip(images, images_ref):
+        assert not compare_atoms(atoms, atoms_ref)
+        for key in ['free_energy', 'forces', 'stress']:
+            np.testing.assert_allclose(
+                atoms.calc.results[key],
+                atoms_ref.calc.results[key],
+                err_msg=key,
+            )
diff -pruN 3.24.0-1/ase/test/fio/castep/test_read_bands.py 3.26.0-1/ase/test/fio/castep/test_read_bands.py
--- 3.24.0-1/ase/test/fio/castep/test_read_bands.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/castep/test_read_bands.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for `read_bands`."""
 import io
 
diff -pruN 3.24.0-1/ase/test/fio/conftest.py 3.26.0-1/ase/test/fio/conftest.py
--- 3.24.0-1/ase/test/fio/conftest.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/conftest.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 
diff -pruN 3.24.0-1/ase/test/fio/exciting/test_exciting.py 3.26.0-1/ase/test/fio/exciting/test_exciting.py
--- 3.24.0-1/ase/test/fio/exciting/test_exciting.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/exciting/test_exciting.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test file for exciting file input and output methods."""
 
 import xml.etree.ElementTree as ET
diff -pruN 3.24.0-1/ase/test/fio/octopus/test_input.py 3.26.0-1/ase/test/fio/octopus/test_input.py
--- 3.24.0-1/ase/test/fio/octopus/test_input.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/octopus/test_input.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/fio/octopus/test_output.py 3.26.0-1/ase/test/fio/octopus/test_output.py
--- 3.24.0-1/ase/test/fio/octopus/test_output.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/octopus/test_output.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for Octopus outputs."""
 from typing import Any, Dict
 
diff -pruN 3.24.0-1/ase/test/fio/test_abinit.py 3.26.0-1/ase/test/fio/test_abinit.py
--- 3.24.0-1/ase/test/fio/test_abinit.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_abinit.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from io import StringIO
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/fio/test_ace.py 3.26.0-1/ase/test/fio/test_ace.py
--- 3.24.0-1/ase/test/fio/test_ace.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_ace.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/fio/test_amber.py 3.26.0-1/ase/test/fio/test_amber.py
--- 3.24.0-1/ase/test/fio/test_amber.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_amber.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/fio/test_animate.py 3.26.0-1/ase/test/fio/test_animate.py
--- 3.24.0-1/ase/test/fio/test_animate.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_animate.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk, fcc111, molecule
 from ase.io.animation import write_animation
 
diff -pruN 3.24.0-1/ase/test/fio/test_atoms_bytes.py 3.26.0-1/ase/test/fio/test_atoms_bytes.py
--- 3.24.0-1/ase/test/fio/test_atoms_bytes.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_atoms_bytes.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk
 from ase.calculators.calculator import compare_atoms
 from ase.io.bytes import parse_atoms, parse_images, to_bytes
diff -pruN 3.24.0-1/ase/test/fio/test_bundletrajectory.py 3.26.0-1/ase/test/fio/test_bundletrajectory.py
--- 3.24.0-1/ase/test/fio/test_bundletrajectory.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_bundletrajectory.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import pytest
diff -pruN 3.24.0-1/ase/test/fio/test_castep.py 3.26.0-1/ase/test/fio/test_castep.py
--- 3.24.0-1/ase/test/fio/test_castep.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_castep.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,202 +0,0 @@
-"""Tests for CASTEP parsers"""
-import io
-import os
-import re
-import warnings
-
-import numpy as np
-import pytest
-
-from ase import Atoms
-from ase.build import molecule
-from ase.calculators.calculator import compare_atoms
-from ase.constraints import FixAtoms, FixCartesian, FixedLine, FixedPlane
-from ase.io import read, write
-from ase.io.castep import read_castep_cell, write_castep_cell
-
-
-# create mol with custom mass - from a list of positions or using
-# ase.build.molecule
-def write_read_atoms(atom, tmp_path):
-    write(os.path.join(tmp_path, "castep_test.cell"), atom)
-    return read(os.path.join(tmp_path, "castep_test.cell"))
-
-
-# write to .cell and check that .cell has correct species_mass block in it
-@pytest.mark.parametrize(
-    "mol, custom_masses, expected_species, expected_mass_block",
-    [
-        ("CH4", {2: [1]}, ["C", "H:0", "H", "H", "H"], ["H:0 2.0"]),
-        ("CH4", {2: [1, 2, 3, 4]}, ["C", "H", "H", "H", "H"], ["H 2.0"]),
-        ("C2H5", {2: [2, 3]}, ["C", "C", "H:0",
-         "H:0", "H", "H", "H"], ["H:0 2.0"]),
-        (
-            "C2H5",
-            {2: [2], 3: [3]},
-            ["C", "C", "H:0", "H:1", "H", "H", "H"],
-            ["H:0 2.0", "H:1 3.0"],
-        ),
-    ],
-)
-def test_custom_mass_write(
-    mol, custom_masses, expected_species, expected_mass_block, tmp_path
-):
-
-    custom_atoms = molecule(mol)
-    atom_positions = custom_atoms.positions
-
-    for mass, indices in custom_masses.items():
-        for i in indices:
-            custom_atoms[i].mass = mass
-
-    atom_masses = custom_atoms.get_masses()
-    # CASTEP IO can be noisy while handling keywords JSON
-    with warnings.catch_warnings():
-        warnings.simplefilter("ignore", category=UserWarning)
-        new_atoms = write_read_atoms(custom_atoms, tmp_path)
-
-    # check atoms have been written and read correctly
-    np.testing.assert_allclose(atom_positions, new_atoms.positions)
-    np.testing.assert_allclose(atom_masses, new_atoms.get_masses())
-
-    # check that file contains appropriate blocks
-    with open(os.path.join(tmp_path, "castep_test.cell")) as f:
-        data = f.read().replace("\n", "\\n")
-
-    position_block = re.search(
-        r"%BLOCK POSITIONS_ABS.*%ENDBLOCK POSITIONS_ABS", data)
-    assert position_block
-
-    pos = position_block.group().split("\\n")[1:-1]
-    species = [p.split(" ")[0] for p in pos]
-    assert species == expected_species
-
-    mass_block = re.search(r"%BLOCK SPECIES_MASS.*%ENDBLOCK SPECIES_MASS", data)
-    assert mass_block
-
-    masses = mass_block.group().split("\\n")[1:-1]
-    for line, expected_line in zip(masses, expected_mass_block):
-        species_name, mass_read = line.split(' ')
-        expected_species_name, expected_mass = expected_line.split(' ')
-        assert pytest.approx(float(mass_read), abs=1e-6) == float(expected_mass)
-        assert species_name == expected_species_name
-
-
-# test setting a custom species on different atom before write
-def test_custom_mass_overwrite(tmp_path):
-    custom_atoms = molecule("CH4")
-    custom_atoms[1].mass = 2
-
-    # CASTEP IO is noisy while handling keywords JSON
-    with warnings.catch_warnings():
-        warnings.simplefilter("ignore", category=UserWarning)
-        atoms = write_read_atoms(custom_atoms, tmp_path)
-
-    # test that changing masses when custom masses defined causes errors
-    atoms[3].mass = 3
-    with pytest.raises(ValueError,
-                       match="Could not write custom mass block for H."):
-        atoms.write(os.path.join(tmp_path, "castep_test2.cell"))
-
-
-# suppress UserWarning due to keyword_tolerance
-@pytest.mark.filterwarnings("ignore::UserWarning")
-class TestConstraints:
-    """Test if the constraint can be recovered when writing and reading.
-
-    Linear constraints in the CASTEP `.cell` format are flexible.
-    The present `read_castep_cell` converts the linear constraints into single
-    FixAtoms for the atoms for which all the three directions are fixed.
-    Otherwise, it makes either `FixedLine` or `FixPlane` depending on the
-    number of fixed directions for each atom.
-    """
-
-    # TODO: test also mask for FixCartesian
-
-    @staticmethod
-    def _make_atoms_ref():
-        """water molecule"""
-        atoms = molecule("H2O")
-        atoms.cell = 10.0 * np.eye(3)
-        atoms.pbc = True
-        atoms.set_initial_magnetic_moments(len(atoms) * [0.0])
-        return atoms
-
-    def _apply_write_read(self, constraint) -> Atoms:
-        atoms_ref = self._make_atoms_ref()
-        atoms_ref.set_constraint(constraint)
-
-        buf = io.StringIO()
-        write_castep_cell(buf, atoms_ref)
-        buf.seek(0)
-        atoms = read_castep_cell(buf)
-
-        assert not compare_atoms(atoms_ref, atoms)
-
-        print(atoms_ref.constraints, atoms.constraints)
-
-        return atoms
-
-    def test_fix_atoms(self):
-        """Test FixAtoms"""
-        constraint = FixAtoms(indices=(1, 2))
-        atoms = self._apply_write_read(constraint)
-
-        assert len(atoms.constraints) == 1
-        assert isinstance(atoms.constraints[0], FixAtoms)
-        assert all(atoms.constraints[0].index == constraint.index)
-
-    def test_fix_cartesian_line(self):
-        """Test FixCartesian along line"""
-        # moved only along the z direction
-        constraint = FixCartesian(0, mask=(1, 1, 0))
-        atoms = self._apply_write_read(constraint)
-
-        assert len(atoms.constraints) == 1
-        for i, idx in enumerate(constraint.index):
-            assert isinstance(atoms.constraints[i], FixedLine)
-            assert atoms.constraints[i].index.tolist() == [idx]
-
-    def test_fix_cartesian_plane(self):
-        """Test FixCartesian in plane"""
-        # moved only in the yz plane
-        constraint = FixCartesian((1, 2), mask=(1, 0, 0))
-        atoms = self._apply_write_read(constraint)
-
-        assert len(atoms.constraints) == 2
-        for i, idx in enumerate(constraint.index):
-            assert isinstance(atoms.constraints[i], FixedPlane)
-            assert atoms.constraints[i].index.tolist() == [idx]
-
-    def test_fix_cartesian_multiple(self):
-        """Test multiple FixCartesian"""
-        constraint = [FixCartesian(1), FixCartesian(2)]
-        atoms = self._apply_write_read(constraint)
-
-        assert len(atoms.constraints) == 1
-        assert isinstance(atoms.constraints[0], FixAtoms)
-        assert atoms.constraints[0].index.tolist() == [1, 2]
-
-    def test_fixed_line(self):
-        """Test FixedLine"""
-        # moved only along the z direction
-        constraint = FixedLine(0, direction=(0, 0, 1))
-        atoms = self._apply_write_read(constraint)
-
-        assert len(atoms.constraints) == 1
-        for i, idx in enumerate(constraint.index):
-            assert isinstance(atoms.constraints[i], FixedLine)
-            assert atoms.constraints[i].index.tolist() == [idx]
-            assert np.allclose(atoms.constraints[i].dir, constraint.dir)
-
-    def test_fixed_plane(self):
-        """Test FixedPlane"""
-        # moved only in the yz plane
-        constraint = FixedPlane((1, 2), direction=(1, 0, 0))
-        atoms = self._apply_write_read(constraint)
-
-        assert len(atoms.constraints) == 2
-        for i, idx in enumerate(constraint.index):
-            assert isinstance(atoms.constraints[i], FixedPlane)
-            assert atoms.constraints[i].index.tolist() == [idx]
-            assert np.allclose(atoms.constraints[i].dir, constraint.dir)
diff -pruN 3.24.0-1/ase/test/fio/test_cfg.py 3.26.0-1/ase/test/fio/test_cfg.py
--- 3.24.0-1/ase/test/fio/test_cfg.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_cfg.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/fio/test_cif.py 3.26.0-1/ase/test/fio/test_cif.py
--- 3.24.0-1/ase/test/fio/test_cif.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_cif.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import io
 import warnings
 
@@ -282,11 +283,8 @@ def test_cif():
     elements = np.unique(atoms_leg.get_atomic_numbers())
     for n in (11, 17, 53):
         assert n in elements
-    try:
-        atoms_leg.info['occupancy']
-        raise AssertionError
-    except KeyError:
-        pass
+
+    assert 'occupancy' not in atoms_leg.info
 
     cif_file = io.StringIO(content)
     # new behavior is to still not read the K atoms, but build info
diff -pruN 3.24.0-1/ase/test/fio/test_cifblock.py 3.26.0-1/ase/test/fio/test_cifblock.py
--- 3.24.0-1/ase/test/fio/test_cifblock.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_cifblock.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.io.cif import CIFBlock, CIFLoop, parse_loop
diff -pruN 3.24.0-1/ase/test/fio/test_cjson.py 3.26.0-1/ase/test/fio/test_cjson.py
--- 3.24.0-1/ase/test/fio/test_cjson.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_cjson.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # test read
 # https://wiki.openchemistry.org/Chemical_JSON
 import numpy as np
diff -pruN 3.24.0-1/ase/test/fio/test_compression.py 3.26.0-1/ase/test/fio/test_compression.py
--- 3.24.0-1/ase/test/fio/test_compression.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_compression.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 Read and write on compressed files.
 """
diff -pruN 3.24.0-1/ase/test/fio/test_cube.py 3.26.0-1/ase/test/fio/test_cube.py
--- 3.24.0-1/ase/test/fio/test_cube.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_cube.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import re
 import tempfile
 
diff -pruN 3.24.0-1/ase/test/fio/test_dftb.py 3.26.0-1/ase/test/fio/test_dftb.py
--- 3.24.0-1/ase/test/fio/test_dftb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_dftb.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # additional tests of the dftb I/O
 from io import StringIO
 
diff -pruN 3.24.0-1/ase/test/fio/test_dlp.py 3.26.0-1/ase/test/fio/test_dlp.py
--- 3.24.0-1/ase/test/fio/test_dlp.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_dlp.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # flake8: noqa
 # tests of the dlpoly I/O
 from io import StringIO
diff -pruN 3.24.0-1/ase/test/fio/test_dmol.py 3.26.0-1/ase/test/fio/test_dmol.py
--- 3.24.0-1/ase/test/fio/test_dmol.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_dmol.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk, molecule
diff -pruN 3.24.0-1/ase/test/fio/test_elk.py 3.26.0-1/ase/test/fio/test_elk.py
--- 3.24.0-1/ase/test/fio/test_elk.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_elk.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import io
 import re
 
diff -pruN 3.24.0-1/ase/test/fio/test_eon_multi_image_read.py 3.26.0-1/ase/test/fio/test_eon_multi_image_read.py
--- 3.24.0-1/ase/test/fio/test_eon_multi_image_read.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_eon_multi_image_read.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # flake8: noqa
 """Check that reading multi image .con files is consistent."""
 
diff -pruN 3.24.0-1/ase/test/fio/test_eon_readwrite.py 3.26.0-1/ase/test/fio/test_eon_readwrite.py
--- 3.24.0-1/ase/test/fio/test_eon_readwrite.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_eon_readwrite.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Check that reading and writing .con files is consistent."""
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/fio/test_espresso.py 3.26.0-1/ase/test/fio/test_espresso.py
--- 3.24.0-1/ase/test/fio/test_espresso.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_espresso.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Quantum ESPRESSO file parsers.
 
 Implemented:
@@ -24,6 +25,7 @@ from ase.io.espresso import (
     write_espresso_in,
     write_fortran_namelist,
 )
+from ase.units import create_units
 
 # This file is parsed correctly by pw.x, even though things are
 # scattered all over the place with some namelist edge cases
@@ -282,6 +284,253 @@ End final coordinates
 
 """
 
+pw_output_cell = """
+     Program PWSCF v.7.4 starts on 30Dec2024 at 16:10:39
+
+     bravais-lattice index     =            0
+     lattice parameter (alat)  =       7.2558  a.u.
+     unit-cell volume          =     270.1072 (a.u.)^3
+     number of atoms/cell      =            2
+     number of atomic types    =            1
+     number of electrons       =         8.00
+     number of Kohn-Sham states=            8
+     kinetic-energy cutoff     =      30.0000  Ry
+     charge density cutoff     =     240.0000  Ry
+     scf convergence threshold =      1.0E-08
+     mixing beta               =       0.3500
+     number of iterations used =            8  local-TF  mixing
+     energy convergence thresh.=      1.0E+00
+     force convergence thresh. =      1.0E-03
+     press convergence thresh. =      1.5E+02
+     Exchange-correlation= PBE
+                           (   1   4   3   4   0   0   0)
+     nstep                     =           50
+
+
+     celldm(1)=   7.255773  celldm(2)=   0.000000  celldm(3)=   0.000000
+     celldm(4)=   0.000000  celldm(5)=   0.000000  celldm(6)=   0.000000
+
+     crystal axes: (cart. coord. in units of alat)
+               a(1) = (   0.000000   0.707107   0.707107 )
+               a(2) = (   0.707107   0.000000   0.707107 )
+               a(3) = (   0.707107   0.707107   0.000000 )
+
+     reciprocal axes: (cart. coord. in units 2 pi/alat)
+               b(1) = ( -0.707107  0.707107  0.707107 )
+               b(2) = (  0.707107 -0.707107  0.707107 )
+               b(3) = (  0.707107  0.707107 -0.707107 )
+
+   Cartesian axes
+
+     site n.     atom                  positions (alat units)
+         1        Si     tau(   1) = (   0.0000000   0.0000000   0.0000000  )
+         2        Si     tau(   2) = (   0.3535534   0.3535534   0.3535534  )
+
+     number of k points=     1  Gaussian smearing, width (Ry)=  0.0010
+                       cart. coord. in units 2pi/alat
+        k(    1) = (   0.0000000   0.0000000   0.0000000), wk =   2.0000000
+
+
+     End of self-consistent calculation
+
+          k = 0.0000 0.0000 0.0000 (   375 PWs)   bands (ev):
+
+    -4.9573   7.1990   7.1990   7.1991   9.4854   9.4854   9.4854  10.6559
+
+     the Fermi energy is     8.8063 ev
+
+!    total energy              =     -21.58743321 Ry
+     estimated scf accuracy    <          1.5E-09 Ry
+     smearing contrib. (-TS)   =      -0.00000000 Ry
+     internal energy E=F+TS    =     -21.58743321 Ry
+
+     The total energy is F=E-TS. E is the sum of the following terms:
+     one-electron contribution =       6.13011013 Ry
+     hartree contribution      =       1.69362248 Ry
+     xc contribution           =     -12.61222208 Ry
+     ewald contribution        =     -16.79894374 Ry
+
+     convergence has been achieved in  11 iterations
+
+     Forces acting on atoms (cartesian axes, Ry/au):
+
+     atom    1 type  1   force =     0.00000000    0.00000000    0.00000000
+     atom    2 type  1   force =     0.00000000    0.00000000    0.00000000
+
+     Total force =     0.000000     Total SCF correction =     0.000007
+
+
+     Computing stress (Cartesian axis) and pressure
+
+          total   stress  (Ry/bohr**3)                   (kbar)     P=      433
+   0.00294455  -0.00000000  -0.00000000          433.16       -0.00       -0.00
+  -0.00000000   0.00294455  -0.00000000           -0.00      433.16       -0.00
+  -0.00000000  -0.00000000   0.00294455           -0.00       -0.00      433.16
+
+
+     BFGS Geometry Optimization
+     Energy error            =      1.6E-01 Ry
+     Gradient error          =      0.0E+00 Ry/Bohr
+     Cell gradient error     =      4.3E+02 kbar
+
+     number of scf cycles    =   1
+     number of bfgs steps    =   0
+
+     enthalpy           new  =     -21.5874332073 Ry
+
+     new trust radius        =       0.2419674028 bohr
+     new conv_thr            =       0.0000000100 Ry
+
+     new unit-cell volume =    334.25681 a.u.^3 (    49.53175 Ang^3 )
+     density =      1.88308 g/cm^3
+
+CELL_PARAMETERS (angstrom)
+  -0.000000000   2.914861274   2.914861274
+   2.914861274  -0.000000000   2.914861274
+   2.914861274   2.914861274  -0.000000000
+
+ATOMIC_POSITIONS (angstrom)
+Si               0.0000000000        0.0000000000        0.0000000000
+Si               1.4574306371        1.4574306371        1.4574306371
+
+     End of self-consistent calculation
+
+          k = 0.0000 0.0000 0.0000 (   375 PWs)   bands (ev):
+
+    -5.7174   5.0283   5.0283   5.0283   6.2048   7.2660   7.2660   7.2660
+
+     the Fermi energy is     5.7451 ev
+
+!    total energy              =     -21.70179088 Ry
+     estimated scf accuracy    <          3.1E-09 Ry
+     smearing contrib. (-TS)   =      -0.00000000 Ry
+     internal energy E=F+TS    =     -21.70179088 Ry
+
+     The total energy is F=E-TS. E is the sum of the following terms:
+     one-electron contribution =       4.44939949 Ry
+     hartree contribution      =       1.82078558 Ry
+     xc contribution           =     -12.32487369 Ry
+     ewald contribution        =     -15.64710226 Ry
+
+     convergence has been achieved in   7 iterations
+
+     Forces acting on atoms (cartesian axes, Ry/au):
+
+     atom    1 type  1   force =     0.00000000    0.00000000   -0.00000000
+     atom    2 type  1   force =     0.00000000    0.00000000   -0.00000000
+
+     Total force =     0.000000     Total SCF correction =     0.000001
+
+
+     Computing stress (Cartesian axis) and pressure
+
+          total   stress  (Ry/bohr**3)                   (kbar)     P=      133
+   0.00090960   0.00000000   0.00000000          133.81        0.00        0.00
+  -0.00000000   0.00090960   0.00000000           -0.00      133.81        0.00
+   0.00000000   0.00000000   0.00090960            0.00        0.00      133.81
+
+     Energy error            =      1.1E-01 Ry
+     Gradient error          =      1.0E-23 Ry/Bohr
+     Cell gradient error     =      1.3E+02 kbar
+
+     bfgs converged in   2 scf cycles and   1 bfgs steps
+     (criteria: energy <  1.0E+00 Ry, force <  1.0E-03 Ry/Bohr, cell <  1.5E+02
+
+     End of BFGS Geometry Optimization
+
+     Final enthalpy           =     -21.7017908769 Ry
+
+     File XXX/tmp-quacc-2024-12-30-15-09-59-202291-63636/pwscf.bfgs deleted, as
+Begin final coordinates
+     new unit-cell volume =    334.25681 a.u.^3 (    49.53175 Ang^3 )
+     density =      1.88308 g/cm^3
+
+CELL_PARAMETERS (angstrom)
+  -0.000000000   2.914861274   2.914861274
+   2.914861274  -0.000000000   2.914861274
+   2.914861274   2.914861274  -0.000000000
+
+ATOMIC_POSITIONS (angstrom)
+Si               0.0000000000        0.0000000000        0.0000000000
+Si               1.4574306371        1.4574306371        1.4574306371
+End final coordinates
+
+     bravais-lattice index     =            0
+     lattice parameter (alat)  =       7.2558  a.u.
+     unit-cell volume          =     334.2568 (a.u.)^3
+     number of atoms/cell      =            2
+     number of atomic types    =            1
+     number of electrons       =         8.00
+     number of Kohn-Sham states=            8
+     kinetic-energy cutoff     =      30.0000  Ry
+     charge density cutoff     =     240.0000  Ry
+     scf convergence threshold =      1.0E-08
+     mixing beta               =       0.3500
+     number of iterations used =            8  local-TF  mixing
+     press convergence thresh. =      1.5E+02
+     Exchange-correlation= PBE
+                           (   1   4   3   4   0   0   0)
+
+     celldm(1)=   7.255773  celldm(2)=   0.000000  celldm(3)=   0.000000
+     celldm(4)=   0.000000  celldm(5)=   0.000000  celldm(6)=   0.000000
+
+     crystal axes: (cart. coord. in units of alat)
+               a(1) = (  -0.000000   0.759160   0.759160 )
+               a(2) = (   0.759160  -0.000000   0.759160 )
+               a(3) = (   0.759160   0.759160  -0.000000 )
+
+     reciprocal axes: (cart. coord. in units 2 pi/alat)
+               b(1) = ( -0.658623  0.658623  0.658623 )
+               b(2) = (  0.658623 -0.658623  0.658623 )
+               b(3) = (  0.658623  0.658623 -0.658623 )
+
+   Cartesian axes
+
+     site n.     atom                  positions (alat units)
+         1        Si     tau(   1) = (   0.0000000   0.0000000   0.0000000  )
+         2        Si     tau(   2) = (   0.3795798   0.3795798   0.3795798  )
+
+     number of k points=     1  Gaussian smearing, width (Ry)=  0.0010
+                       cart. coord. in units 2pi/alat
+        k(    1) = (   0.0000000   0.0000000   0.0000000), wk =   2.0000000
+
+     End of self-consistent calculation
+
+          k = 0.0000 0.0000 0.0000 (   471 PWs)   bands (ev):
+
+    -5.7176   5.0274   5.0274   5.0274   6.2042   7.2647   7.2647   7.2647
+
+     the Fermi energy is     5.7439 ev
+
+!    total energy              =     -21.70223615 Ry
+     estimated scf accuracy    <          4.9E-10 Ry
+     smearing contrib. (-TS)   =      -0.00000000 Ry
+     internal energy E=F+TS    =     -21.70223615 Ry
+
+     The total energy is F=E-TS. E is the sum of the following terms:
+     one-electron contribution =       4.44893174 Ry
+     hartree contribution      =       1.82082186 Ry
+     xc contribution           =     -12.32488755 Ry
+     ewald contribution        =     -15.64710220 Ry
+
+     convergence has been achieved in   9 iterations
+
+     Forces acting on atoms (cartesian axes, Ry/au):
+
+     atom    1 type  1   force =     0.00000000    0.00000000    0.00000000
+     atom    2 type  1   force =     0.00000000    0.00000000    0.00000000
+
+     Total force =     0.000000     Total SCF correction =     0.000002
+
+
+     Computing stress (Cartesian axis) and pressure
+
+          total   stress  (Ry/bohr**3)                   (kbar)     P=      134
+   0.00091401  -0.00000000  -0.00000000          134.46       -0.00       -0.00
+  -0.00000000   0.00091401  -0.00000000           -0.00      134.46       -0.00
+  -0.00000000  -0.00000000   0.00091401           -0.00       -0.00      134.46
+ """
+
 
 def test_pw_input():
     """Read pw input file."""
@@ -321,6 +570,31 @@ def test_pw_output():
     assert pw_output_traj[1].get_volume() > pw_output_traj[0].get_volume()
 
 
+def test_pw_output_cell():
+    """Read pw output file with cell optimization."""
+    with open('pw_output.pwo', 'w') as pw_output_f:
+        pw_output_f.write(pw_output_cell)
+
+    pw_output_traj = ase.io.read('pw_output.pwo', index=':')
+    assert len(pw_output_traj) == 3
+
+    units = create_units('2006')
+    expected_first_cell = units["Bohr"] * 7.255773 * np.array(
+        [[0.000000, 0.707107, 0.707107],
+         [0.707107, 0.000000, 0.707107],
+         [0.707107, 0.707107, 0.000000]]
+    )
+
+    expected_second_cell = np.array(
+        [[-0.000000, 2.914861274, 2.914861274],
+         [2.914861274, -0.000000, 2.914861274],
+         [2.914861274, 2.914861274, -0.000000]]
+    )
+
+    assert np.allclose(pw_output_traj[0].cell, expected_first_cell)
+    assert np.allclose(pw_output_traj[1].cell, expected_second_cell)
+
+
 def test_pw_parse_line():
     """Parse a single position line from a pw.x output file."""
     txt = """       994           Pt  tau( 994) = \
@@ -415,7 +689,8 @@ def test_pw_input_write_raw_kpts():
 
     fh = 'espresso_test.pwi'
     pseudos = {'Ni': 'potato', 'O': 'orange'}
-    kpts = np.random.random((10, 4))
+    rng = np.random.RandomState(42)
+    kpts = rng.random((10, 4))
 
     write_espresso_in(fh, bulk, pseudopotentials=pseudos, kpts=kpts)
     readback = read_espresso_in('espresso_test.pwi')
diff -pruN 3.24.0-1/ase/test/fio/test_espresso_ph.py 3.26.0-1/ase/test/fio/test_espresso_ph.py
--- 3.24.0-1/ase/test/fio/test_espresso_ph.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_espresso_ph.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from io import StringIO
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/fio/test_external_io_formats.py 3.26.0-1/ase/test/fio/test_external_io_formats.py
--- 3.24.0-1/ase/test/fio/test_external_io_formats.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_external_io_formats.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 Tests of the plugin functionality for defining IO formats
 outside of the ase package
diff -pruN 3.24.0-1/ase/test/fio/test_extxyz.py 3.26.0-1/ase/test/fio/test_extxyz.py
--- 3.24.0-1/ase/test/fio/test_extxyz.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_extxyz.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # additional tests of the extended XYZ file I/O
 # (which is also included in oi.py test case)
 # maintained by James Kermode <james.kermode@gmail.com>
@@ -324,14 +325,17 @@ def test_json_scalars():
     assert abs(b.info['val_3'] - 42) == 0
 
 
+@pytest.mark.parametrize(
+    'columns',
+    [None, ['symbols', 'positions', 'move_mask']],
+)
 @pytest.mark.parametrize('constraint', [FixAtoms(indices=(0, 2)),
                                         FixCartesian(1, mask=(1, 0, 1)),
                                         [FixCartesian(0), FixCartesian(2)]])
-def test_constraints(constraint):
+def test_constraints(constraint, columns):
     atoms = molecule('H2O')
     atoms.set_constraint(constraint)
 
-    columns = ['symbols', 'positions', 'move_mask']
     ase.io.write('tmp.xyz', atoms, columns=columns)
 
     atoms2 = ase.io.read('tmp.xyz')
@@ -501,3 +505,10 @@ def test_linear_combination_calculator()
     atoms.calc = LinearCombinationCalculator([EMT()], [1.0])
     atoms.get_potential_energy()
     atoms.write('tmp.xyz')
+
+
+def test_outputs_not_properties(tmp_path):
+    atoms = Atoms('Cu2', cell=[4, 2, 2], positions=[[0, 0, 0], [2.05, 0, 0]],
+                  pbc=[True] * 3, info={'nbands': 1})
+    ase.io.write(tmp_path / 'nbands.extxyz', atoms)
+    _ = ase.io.read(tmp_path / 'nbands.extxyz')
diff -pruN 3.24.0-1/ase/test/fio/test_fileobj.py 3.26.0-1/ase/test/fio/test_fileobj.py
--- 3.24.0-1/ase/test/fio/test_fileobj.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_fileobj.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # test reading and writing a file descriptor using its name
 
 import pytest
diff -pruN 3.24.0-1/ase/test/fio/test_formats.py 3.26.0-1/ase/test/fio/test_formats.py
--- 3.24.0-1/ase/test/fio/test_formats.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_formats.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from pathlib import Path
 
 import pytest
diff -pruN 3.24.0-1/ase/test/fio/test_gaussian.py 3.26.0-1/ase/test/fio/test_gaussian.py
--- 3.24.0-1/ase/test/fio/test_gaussian.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_gaussian.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import copy
 from io import StringIO
 
diff -pruN 3.24.0-1/ase/test/fio/test_gaussian_gjf_input.py 3.26.0-1/ase/test/fio/test_gaussian_gjf_input.py
--- 3.24.0-1/ase/test/fio/test_gaussian_gjf_input.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_gaussian_gjf_input.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 test_gaussian_gjf_input.py, Geoffrey Weal, 23/5/24
 
diff -pruN 3.24.0-1/ase/test/fio/test_gaussian_out.py 3.26.0-1/ase/test/fio/test_gaussian_out.py
--- 3.24.0-1/ase/test/fio/test_gaussian_out.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_gaussian_out.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for the gaussian-out format."""
 from io import StringIO
 
diff -pruN 3.24.0-1/ase/test/fio/test_gen.py 3.26.0-1/ase/test/fio/test_gen.py
--- 3.24.0-1/ase/test/fio/test_gen.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_gen.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for GEN format"""
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/fio/test_gpaw.py 3.26.0-1/ase/test/fio/test_gpaw.py
--- 3.24.0-1/ase/test/fio/test_gpaw.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_gpaw.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import io
 
 from ase.io import read
@@ -10,6 +11,13 @@ header = """
  |___|_|
 """
 
+densities = """
+Densities:
+  Coarse grid: 32*32*32 grid
+  Fine grid: 64*64*64 grid
+  Total Charge: 1.000000
+"""
+
 atoms = """
 Reference energy: -26313.685229
 
@@ -37,6 +45,14 @@ Free energy:    -10.229926
 Extrapolated:   -10.038965
 """
 
+orbitals = """
+ Band  Eigenvalues  Occupancy
+    0     -6.19111    2.00000
+    1      2.15616    0.33333
+    2      2.15616    0.33333
+    3      2.15616    0.33333
+"""
+
 forces = """
 Forces in eV/Ang:
   0 Al    0.00000    0.00000   -0.00000
@@ -49,7 +65,8 @@ Stress tensor:
      0.000000     0.000000     0.000000"""
 
 # Three configurations.  Only 1. and 3. has forces.
-text = header + atoms + forces + atoms + atoms + forces + stress
+text = (header + densities + atoms + orbitals + forces +
+        atoms + atoms + forces + stress)
 
 
 def test_gpaw_output():
@@ -61,3 +78,8 @@ def test_gpaw_output():
     fd = io.StringIO(text)
     configs = read(fd, index=':', format='gpaw-out')
     assert len(configs) == 3
+
+    for config in configs:
+        assert config.get_initial_charges().sum() == 1
+
+    assert len(configs[0].calc.get_eigenvalues()) == 4
diff -pruN 3.24.0-1/ase/test/fio/test_gpumd.py 3.26.0-1/ase/test/fio/test_gpumd.py
--- 3.24.0-1/ase/test/fio/test_gpumd.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_gpumd.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """GPUMD input file parser.
 
 Implemented:
diff -pruN 3.24.0-1/ase/test/fio/test_info.py 3.26.0-1/ase/test/fio/test_info.py
--- 3.24.0-1/ase/test/fio/test_info.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_info.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.io import Trajectory
 
diff -pruN 3.24.0-1/ase/test/fio/test_ioformats.py 3.26.0-1/ase/test/fio/test_ioformats.py
--- 3.24.0-1/ase/test/fio/test_ioformats.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_ioformats.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.io.formats import ioformats
diff -pruN 3.24.0-1/ase/test/fio/test_iread_path.py 3.26.0-1/ase/test/fio/test_iread_path.py
--- 3.24.0-1/ase/test/fio/test_iread_path.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_iread_path.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from pathlib import Path
 
 import pytest
diff -pruN 3.24.0-1/ase/test/fio/test_json_arrays.py 3.26.0-1/ase/test/fio/test_json_arrays.py
--- 3.24.0-1/ase/test/fio/test_json_arrays.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_json_arrays.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.io.jsonio import decode, encode
diff -pruN 3.24.0-1/ase/test/fio/test_jsonio.py 3.26.0-1/ase/test/fio/test_jsonio.py
--- 3.24.0-1/ase/test/fio/test_jsonio.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_jsonio.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import io
 from datetime import datetime
 
@@ -9,6 +10,7 @@ from ase.io.jsonio import decode, encode
 def test_jsonio():
     """Test serialization of ndarrays and other stuff."""
     assert decode(encode(np.int64(42))) == 42
+    assert decode(encode(np.float32(42.0))) == 42.0
 
     c = np.array([0.1j])
     assert (decode(encode(c)) == c).all()
diff -pruN 3.24.0-1/ase/test/fio/test_jsonio_atoms.py 3.26.0-1/ase/test/fio/test_jsonio_atoms.py
--- 3.24.0-1/ase/test/fio/test_jsonio_atoms.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_jsonio_atoms.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
@@ -59,7 +60,7 @@ def test_jsonio_constraints_cartesian(si
     assert np.array_equal(c1[0].mask, c2[0].mask)
 
 
-def test_jsonio_constraints_fix_atoms_empty(silver_bulk):
+def test_jsonio_constraints_fix_atoms_empty(silver_bulk: Atoms) -> None:
     a = np.empty(0, dtype=int)
     silver_bulk.set_constraint(FixAtoms(a))
     new_atoms: Atoms = decode(encode(silver_bulk))
diff -pruN 3.24.0-1/ase/test/fio/test_lammpsdump.py 3.26.0-1/ase/test/fio/test_lammpsdump.py
--- 3.24.0-1/ase/test/fio/test_lammpsdump.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_lammpsdump.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
@@ -34,11 +35,16 @@ def lammpsdump():
                 position_cols="x y z",
                 have_element=True,
                 have_id=False,
-                have_type=True):
+                have_type=True,
+                have_property_atom=False):
 
         _element = "element" if have_element else "unk0"
         _id = "id" if have_id else "unk1"
         _type = "type" if have_type else "unk2"
+        _i_property = "i_property" if have_property_atom else "unk3"
+        _i2_property = "i2_property[1]" if have_property_atom else "unk4"
+        _d_property = "d_property" if have_property_atom else "unk5"
+        _d2_property = "d2_property[1]" if have_property_atom else "unk6"
 
         buf = f"""\
         ITEM: TIMESTEP
@@ -49,10 +55,11 @@ def lammpsdump():
         0.0e+00 4e+00
         0.0e+00 5.0e+00
         0.0e+00 2.0e+01
-        ITEM: ATOMS {_element} {_id} {_type} {position_cols}
-        C  1 1 0.5 0.6 0.7
-        C  3 1 0.6 0.1 1.9
-        Si 2 2 0.45 0.32 0.67
+        ITEM: ATOMS {_element} {_id} {_type} {position_cols}\
+        {_i_property} {_i2_property} {_d_property} {_d2_property}
+        C  1 1 0.5 0.6 0.7 1 2 1.0 2.0
+        C  3 1 0.6 0.1 1.9 3 4 3.0 4.0
+        Si 2 2 0.45 0.32 0.67 5 6 5.0 6.0
         """
 
         return buf
@@ -92,6 +99,35 @@ def lammpsdump_single_atom():
     return factory
 
 
+@pytest.fixture()
+def lammpsdump_no_element():
+    def factory(bounds="pp pp pp",
+                position_cols="x y z",
+                have_id=True,
+                have_type=True,
+                have_mass=True):
+
+        _id = "id" if have_id else "unk1"
+        _type = "type" if have_type else "unk2"
+        _mass = "mass" if have_mass else "unk3"
+
+        buf = f"""\
+        ITEM: TIMESTEP
+        100
+        ITEM: NUMBER OF ATOMS
+        1
+        ITEM: BOX BOUNDS {bounds}
+        0.0e+00 4e+00
+        0.0e+00 5.0e+00
+        0.0e+00 2.0e+01
+        ITEM: ATOMS {_id} {_type} {_mass} {position_cols}
+        1 1 12 0.5 0.6 0.7
+        """
+        return buf
+
+    return factory
+
+
 def lammpsdump_headers():
     actual_magic = 'ITEM: TIMESTEP'
     yield actual_magic
@@ -120,6 +156,19 @@ def test_lammpsdump_element(fmt, lammpsd
     assert np.all(atoms.get_atomic_numbers() == np.array([6, 6, 14]))
 
 
+def test_lammpsdump_custom_property(fmt, lammpsdump):
+    # Test lammpsdump with custom property column given
+    atoms = fmt.parse_atoms(lammpsdump(have_property_atom=True))
+    assert np.all(atoms.arrays['i_property'].flatten() ==
+                  np.array([1, 3, 5]))
+    assert np.all(atoms.arrays['i2_property[1]'].flatten() ==
+                  np.array([2, 4, 6]))
+    assert np.all(atoms.arrays['d_property'].flatten() ==
+                  np.array([1., 3., 5.]))
+    assert np.all(atoms.arrays['d2_property[1]'].flatten() ==
+                  np.array([2., 4., 6.]))
+
+
 def test_lammpsdump_single_atom(fmt, lammpsdump_single_atom):
     # Test lammpsdump with a single atom
     atoms = fmt.parse_atoms(lammpsdump_single_atom())
@@ -127,6 +176,13 @@ def test_lammpsdump_single_atom(fmt, lam
     assert pytest.approx(atoms.get_initial_charges()) == np.array([1.])
 
 
+def test_lammpsdump_no_element(fmt, lammpsdump_no_element):
+    # Test lammpsdump with no element column
+    atoms = fmt.parse_atoms(lammpsdump_no_element())
+    assert atoms.info['timestep'] == 100
+    assert np.all(atoms.get_chemical_symbols() == np.array(['C']))
+
+
 def test_lammpsdump_errors(fmt, lammpsdump):
     # elements not given
     with pytest.raises(ValueError,
diff -pruN 3.24.0-1/ase/test/fio/test_magmom.py 3.26.0-1/ase/test/fio/test_magmom.py
--- 3.24.0-1/ase/test/fio/test_magmom.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_magmom.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.io import read, write
 
diff -pruN 3.24.0-1/ase/test/fio/test_magres.py 3.26.0-1/ase/test/fio/test_magres.py
--- 3.24.0-1/ase/test/fio/test_magres.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_magres.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,9 +1,50 @@
+# fmt: off
 import numpy as np
+import pytest
 
 from ase.build import bulk
 from ase.calculators.singlepoint import SinglePointDFTCalculator
 from ase.io import read, write
 
+magres_with_complex_labels = """#$magres-abinitio-v1.0
+# Fictional test data
+[atoms]
+units lattice Angstrom
+lattice       8.0 0.0 0.0 0.0 10.2 0.0 0.0 0.0 15.9
+units atom Angstrom
+atom H H1   1    3.70 4.9 1.07
+atom H H2   100  4.30 2.1 5.15
+atom H H2a  101  4.30 2.1 5.15
+atom H H2b  102  4.30 2.1 5.15
+[/atoms]
+[magres]
+units ms ppm
+ms H1  1    3.0 -5.08 -3.19 -4.34 3.42 -3.78  1.05 -1.89  2.85
+ms H2100    3.0  5.08  3.19  4.34 3.42 -3.78 -1.05 -1.89  2.85
+ms H2a101   3.0  5.08  3.19  4.34 3.42 -3.78 -1.05 -1.89  2.85
+ms H2b 102  3.0  5.08  3.19  4.34 3.42 -3.78 -1.05 -1.89  2.85
+units efg au
+efg H1  1   9.6 -3.43  1.45 -3.43 8.52 -1.43  1.45 -1.43 -1.81
+efg H2100   9.6  3.43 -1.45  3.43 8.52 -1.43 -1.45 -1.43 -1.81
+efg H2a101  9.6  3.43 -1.45  3.43 8.52 -1.43 -1.45 -1.43 -1.81
+efg H2b 102 9.6  3.43 -1.45  3.43 8.52 -1.43 -1.45 -1.43 -1.81
+[/magres]
+"""
+
+magres_with_too_large_index = """#$magres-abinitio-v1.0
+# Test data with index >999
+[atoms]
+units lattice Angstrom
+lattice       10.0 0.0 0.0 0.0 10.0 0.0 0.0 0.0 10.0
+units atom Angstrom
+atom H H1 1000    0.0 0.0 0.0
+[/atoms]
+[magres]
+units ms ppm
+ms H11000    1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 1.0
+[/magres]
+"""
+
 
 def test_magres():
 
@@ -35,3 +76,34 @@ def test_magres_large(datadir):
 
     # Test with big structure
     assert len(read(datadir / "large_atoms.magres")) == 240
+
+
+def test_magres_sitelabels(datadir):
+    """Test reading magres files with munged site labels and indices
+    for cases where the site label has a number in it."""
+
+    # Write temporary file
+    with open('magres_with_complex_labels.magres', 'w') as f:
+        f.write(magres_with_complex_labels)
+
+    # Read it back
+    atoms = read('magres_with_complex_labels.magres')
+
+    labels_ref = ['H1', 'H2', 'H2a', 'H2b']
+    labels = atoms.get_array('labels')
+    np.testing.assert_array_equal(labels, labels_ref)
+
+    indices_ref = [1, 100, 101, 102]
+    indices = atoms.get_array('indices')
+    np.testing.assert_array_equal(indices, indices_ref)
+
+
+def test_magres_with_large_indices():
+    """Test handling of magres files with indices >999"""
+    # Write temporary file
+    with open('magres_large_index.magres', 'w') as f:
+        f.write(magres_with_too_large_index)
+
+    # Check that reading raises the correct error
+    with pytest.raises(RuntimeError, match="Index greater than 999 detected"):
+        read('magres_large_index.magres')
diff -pruN 3.24.0-1/ase/test/fio/test_match_magic.py 3.26.0-1/ase/test/fio/test_match_magic.py
--- 3.24.0-1/ase/test/fio/test_match_magic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_match_magic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.io.formats import ioformats
 
 
diff -pruN 3.24.0-1/ase/test/fio/test_mustem.py 3.26.0-1/ase/test/fio/test_mustem.py
--- 3.24.0-1/ase/test/fio/test_mustem.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_mustem.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/fio/test_netcdftrajectory.py 3.26.0-1/ase/test/fio/test_netcdftrajectory.py
--- 3.24.0-1/ase/test/fio/test_netcdftrajectory.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_netcdftrajectory.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import warnings
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/fio/test_nomad.py 3.26.0-1/ase/test/fio/test_nomad.py
--- 3.24.0-1/ase/test/fio/test_nomad.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_nomad.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # Stripped (minimal) version of nomad entry with 3 images.
 # The images are actually identical for some reason, but we want to be sure
 # that they are extracted correctly.
diff -pruN 3.24.0-1/ase/test/fio/test_nwchem.py 3.26.0-1/ase/test/fio/test_nwchem.py
--- 3.24.0-1/ase/test/fio/test_nwchem.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_nwchem.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/fio/test_oi.py 3.26.0-1/ase/test/fio/test_oi.py
--- 3.24.0-1/ase/test/fio/test_oi.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_oi.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import warnings
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/fio/test_oldtraj.py 3.26.0-1/ase/test/fio/test_oldtraj.py
--- 3.24.0-1/ase/test/fio/test_oldtraj.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_oldtraj.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test that we can read old trajectory files."""
 from base64 import b64decode, b64encode
 from pathlib import Path
diff -pruN 3.24.0-1/ase/test/fio/test_onetep.py 3.26.0-1/ase/test/fio/test_onetep.py
--- 3.24.0-1/ase/test/fio/test_onetep.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_onetep.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """ONETEP file parsers.
 
 Implemented:
diff -pruN 3.24.0-1/ase/test/fio/test_openmx.py 3.26.0-1/ase/test/fio/test_openmx.py
--- 3.24.0-1/ase/test/fio/test_openmx.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_openmx.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import io
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/fio/test_opls.py 3.26.0-1/ase/test/fio/test_opls.py
--- 3.24.0-1/ase/test/fio/test_opls.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_opls.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 The OPLS io module uses the coordinatetransform.py module under
 calculators/lammps/.  This test simply ensures that it uses that module
diff -pruN 3.24.0-1/ase/test/fio/test_orca.py 3.26.0-1/ase/test/fio/test_orca.py
--- 3.24.0-1/ase/test/fio/test_orca.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_orca.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import io
 
 import numpy as np
@@ -5,6 +6,7 @@ import pytest
 
 from ase.atoms import Atoms
 from ase.calculators.calculator import compare_atoms
+from ase.io import read
 from ase.io.orca import (
     read_dipole,
     read_energy,
@@ -92,6 +94,15 @@ MULLIKEN ATOMIC CHARGES AND SPIN POPULAT
 Sum of atomic charges         :   -0.0000000
 Sum of atomic spin populations:    1.0000000
 
+Number of atoms                             ...      3
+
+ ---------------------------------                                               
+ CARTESIAN COORDINATES (ANGSTROEM)                                               
+ ---------------------------------
+   O     0.0000000    0.0000000    0.0000000
+   H     1.8897261    0.0000000    0.0000000
+   H     0.0000000    1.8897261    0.0000000
+
  -------
 TIMINGS
 -------
@@ -120,7 +131,8 @@ Grid generation             ....       0
 -------------------------   --------------------
 FINAL SINGLE POINT ENERGY       -76.422436201230
 -------------------------   --------------------
-"""
+ORCA TERMINATED NORMALLY
+"""  # noqa: E501, W291
 
     sample_engradfile = """\
 #
@@ -165,7 +177,8 @@ FINAL SINGLE POINT ENERGY       -76.4224
 
     results_sample['free_energy'] = results_sample['energy']
 
-    results = read_orca_outputs('.', 'orcamolecule_test.out')
+    with pytest.warns(DeprecationWarning):
+        results = read_orca_outputs('.', 'orcamolecule_test.out')
 
     keys = set(results)
     assert keys == set(results_sample)
@@ -205,3 +218,64 @@ FINAL SINGLE POINT ENERGY      -815.9597
     energy = read_energy(io.StringIO(text))
     energy_ref = -815.959737266080 * Hartree
     assert energy == energy_ref
+
+
+def test_read_orca_output_file():
+    sample_outputfile = """\
+    
+                                 *****************
+                                 * O   R   C   A *
+                                 *****************
+
+                     *******************************
+                     * Energy+Gradient Calculation *
+                     *******************************
+
+---------------------------------
+CARTESIAN COORDINATES (ANGSTROEM)
+---------------------------------
+   O     0.0000000    0.0000000    0.0000000
+   H     1.8897261    0.0000000    0.0000000
+   H     0.0000000    1.8897261    0.0000000
+
+----------------------
+SHARK INTEGRAL PACKAGE
+----------------------
+
+Number of atoms                             ...      3
+
+
+------------------
+CARTESIAN GRADIENT
+------------------
+
+   1   O   :   -0.047131485   -0.047131485    0.0000000001
+   2   H   :    0.025621056    0.021510429    0.0000000000
+   3   H   :    0.021510429    0.025621056   -0.0000000001
+
+-------------------------   --------------------
+FINAL SINGLE POINT ENERGY       -76.422436201230
+-------------------------   --------------------
+ORCA TERMINATED NORMALLY
+"""  # noqa: W293
+    with open('orca_test.out', 'w') as fd:
+        fd.write(sample_outputfile)
+
+    results_sample = {
+        'energy': -2079.560412394247,
+        'forces': np.array([
+            [2.42359838e+00, 2.42359837e+00, -5.14220671e-09],
+            [-1.31748766e+00, -1.10611070e+00, -0.0000000e-00],
+            [-1.10611071e+00, -1.31748767e+00, 5.14220671e-09]]),
+        'positions': np.array([
+            [0.0000000, 0.0000000, 0.0000000],
+            [1.8897261, 0.0000000, 0.0000000],
+            [0.0000000, 1.8897261, 0.0000000]])}
+
+    results_sample['free_energy'] = results_sample['energy']
+
+    atoms = read('orca_test.out')
+
+    assert results_sample['energy'] == pytest.approx(atoms.get_total_energy())
+    assert results_sample['forces'] == pytest.approx(atoms.get_forces())
+    assert results_sample['positions'] == pytest.approx(atoms.get_positions())
diff -pruN 3.24.0-1/ase/test/fio/test_parallel.py 3.26.0-1/ase/test/fio/test_parallel.py
--- 3.24.0-1/ase/test/fio/test_parallel.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_parallel.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.io import read, write
 from ase.parallel import world
diff -pruN 3.24.0-1/ase/test/fio/test_pdb_cell_io.py 3.26.0-1/ase/test/fio/test_pdb_cell_io.py
--- 3.24.0-1/ase/test/fio/test_pdb_cell_io.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_pdb_cell_io.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/fio/test_pdb_extra.py 3.26.0-1/ase/test/fio/test_pdb_extra.py
--- 3.24.0-1/ase/test/fio/test_pdb_extra.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_pdb_extra.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """PDB parser
 
 Test dealing with files that are not fully
diff -pruN 3.24.0-1/ase/test/fio/test_pickle_bundle_trajectory.py 3.26.0-1/ase/test/fio/test_pickle_bundle_trajectory.py
--- 3.24.0-1/ase/test/fio/test_pickle_bundle_trajectory.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_pickle_bundle_trajectory.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import sys
 from pathlib import Path
 from subprocess import check_call, check_output
diff -pruN 3.24.0-1/ase/test/fio/test_plotting_variables.py 3.26.0-1/ase/test/fio/test_plotting_variables.py
--- 3.24.0-1/ase/test/fio/test_plotting_variables.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_plotting_variables.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,125 @@
+# fmt: off
+
+
+import numpy as np
+import pytest
+from scipy.spatial.transform import Rotation
+
+from ase import Atoms
+from ase.io.utils import PlottingVariables
+
+
+@pytest.fixture
+def atoms_in_cell():
+    atoms = Atoms('H3',
+                positions=[
+                    [0.0, 0, 0],
+                    [0.3, 0, 0],
+                    [0.8, 0, 0]],
+                cell=[1, 1, 1],
+                pbc=True)
+
+    return atoms
+
+
+@pytest.fixture
+def random_rotation():
+    myrng = np.random.default_rng(seed=453)
+    random_rotation = Rotation.random(random_state=myrng)
+    random_rotation_matrix = random_rotation.as_matrix()
+
+    return random_rotation_matrix
+
+
+def test_set_bbox(atoms_in_cell):
+
+    rotation = '0x, 0y, 0z'
+
+    generic_projection_settings = {
+        'rotation': rotation,
+        'bbox': (0, 0, 1, 1),
+        'show_unit_cell': 2}
+
+    pl = PlottingVariables(atoms=atoms_in_cell, **generic_projection_settings)
+
+    camera_location = pl.get_image_plane_center()
+    assert np.allclose(camera_location, [0.5, 0.5, 1.0])
+
+    bbox2 = [0, 0, 0.5, 0.5]
+    pl.update_image_plane_offset_and_size_from_structure(
+        bbox=bbox2)
+
+    camera_location = pl.get_image_plane_center()
+
+    assert np.allclose(camera_location, [0.25, 0.25, 1.0])
+    assert np.allclose(pl.get_bbox(), bbox2)
+
+
+def test_camera_directions(atoms_in_cell):
+
+    rotation = '0x, 45y, 0z'
+
+    generic_projection_settings = {'rotation': rotation}
+
+    pl = PlottingVariables(atoms=atoms_in_cell, **generic_projection_settings)
+
+    camdir = pl.get_camera_direction()
+    up = pl.get_camera_up()
+    right = pl.get_camera_right()
+
+    assert np.allclose(camdir.T @ up, 0)
+    assert np.allclose(camdir.T @ right, 0)
+    assert np.allclose(right.T @ up, 0)
+
+    r22 = np.sqrt(2) / 2
+    assert np.allclose(camdir, [r22, 0, -r22])
+    assert np.allclose(up, [0, 1, 0])
+    assert np.allclose(right, [r22, 0, r22])
+
+
+def test_set_rotation_from_camera_directions(atoms_in_cell):
+    '''Looks down the <111> direction'''
+    generic_projection_settings = {
+        'show_unit_cell': 2}
+
+    pl = PlottingVariables(atoms=atoms_in_cell, **generic_projection_settings)
+
+    pl.set_rotation_from_camera_directions(
+        look=[-1, -1, -1], up=None, right=[-1, 1, 0],
+        scaled_position=True)
+
+    camdir = pl.get_camera_direction()
+    up = pl.get_camera_up()
+    right = pl.get_camera_right()
+
+    invrt3 = 1 / np.sqrt(3)
+    invrt2 = 1 / np.sqrt(2)
+    assert np.allclose(right, [-invrt2, invrt2, 0])
+    assert np.allclose(camdir, [-invrt3, -invrt3, -invrt3])
+    assert np.allclose(up, [-1 / np.sqrt(6), -1 / np.sqrt(6), np.sqrt(2 / 3)])
+
+
+def test_center_camera_on_position(atoms_in_cell):
+    '''look at the upper left corner, camera should be above that point'''
+
+    generic_projection_settings = {'show_unit_cell': 2}
+    pl = PlottingVariables(atoms=atoms_in_cell, **generic_projection_settings)
+    pl.center_camera_on_position([1, 1, 0])
+    camera_location = pl.get_image_plane_center()
+
+    assert np.allclose(camera_location, [1, 1, 1])
+
+
+def test_camera_string_with_random_rotation(atoms_in_cell, random_rotation):
+    '''Checks that a randome rotation matrix can be converted to a Euler
+    rotation string and back'''
+
+    random_rotation_matrix = random_rotation
+    generic_projection_settings = {'show_unit_cell': 2,
+                                   'rotation': random_rotation_matrix}
+    pl = PlottingVariables(atoms=atoms_in_cell, **generic_projection_settings)
+
+    rotation_string = pl.get_rotation_angles_string()
+    pl.set_rotation(rotation_string)
+    # higher atol since the default digits are 5
+    assert np.allclose(random_rotation_matrix, pl.rotation, atol=1e-07)
diff -pruN 3.24.0-1/ase/test/fio/test_povray.py 3.26.0-1/ase/test/fio/test_povray.py
--- 3.24.0-1/ase/test/fio/test_povray.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_povray.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from subprocess import DEVNULL, check_call
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/fio/test_prismatic.py 3.26.0-1/ase/test/fio/test_prismatic.py
--- 3.24.0-1/ase/test/fio/test_prismatic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_prismatic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/fio/test_proteindatabank.py 3.26.0-1/ase/test/fio/test_proteindatabank.py
--- 3.24.0-1/ase/test/fio/test_proteindatabank.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_proteindatabank.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # flake8: noqa
 from io import StringIO
 
diff -pruN 3.24.0-1/ase/test/fio/test_py.py 3.26.0-1/ase/test/fio/test_py.py
--- 3.24.0-1/ase/test/fio/test_py.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_py.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for the .py format."""
 import io
 
diff -pruN 3.24.0-1/ase/test/fio/test_pycodcif_read.py 3.26.0-1/ase/test/fio/test_pycodcif_read.py
--- 3.24.0-1/ase/test/fio/test_pycodcif_read.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_pycodcif_read.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.io.cif import read_cif
diff -pruN 3.24.0-1/ase/test/fio/test_qbox.py 3.26.0-1/ase/test/fio/test_qbox.py
--- 3.24.0-1/ase/test/fio/test_qbox.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_qbox.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests related to QBOX"""
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/fio/test_readwrite_errors.py 3.26.0-1/ase/test/fio/test_readwrite_errors.py
--- 3.24.0-1/ase/test/fio/test_readwrite_errors.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_readwrite_errors.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import warnings
 from io import StringIO
 
diff -pruN 3.24.0-1/ase/test/fio/test_res.py 3.26.0-1/ase/test/fio/test_res.py
--- 3.24.0-1/ase/test/fio/test_res.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_res.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.atoms import Atoms
 from ase.calculators.singlepoint import SinglePointCalculator
 from ase.io.res import Res, read_res, write_res
diff -pruN 3.24.0-1/ase/test/fio/test_rmc6f.py 3.26.0-1/ase/test/fio/test_rmc6f.py
--- 3.24.0-1/ase/test/fio/test_rmc6f.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_rmc6f.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # type: ignore
 import numpy as np
 
@@ -221,7 +222,7 @@ def test_rmc6f_write_output():
     """
     header_lines = [
         '(Version 6f format configuration file)',
-        '(Generated by ASE - Atomic Simulation Environment https://wiki.fysik.dtu.dk/ase/ )',  # noqa: E501
+        '(Generated by ASE - Atomic Simulation Environment https://ase-lib.org/ )',  # noqa: E501
         "Metadata date:18-007-'2019",
         'Number of types of atoms:   2 ',
         'Atom types present:          S F',
diff -pruN 3.24.0-1/ase/test/fio/test_sdf.py 3.26.0-1/ase/test/fio/test_sdf.py
--- 3.24.0-1/ase/test/fio/test_sdf.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_sdf.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import io
 
 import pytest
diff -pruN 3.24.0-1/ase/test/fio/test_siesta.py 3.26.0-1/ase/test/fio/test_siesta.py
--- 3.24.0-1/ase/test/fio/test_siesta.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_siesta.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from io import StringIO
 from pathlib import Path
 
diff -pruN 3.24.0-1/ase/test/fio/test_traj_bytesio.py 3.26.0-1/ase/test/fio/test_traj_bytesio.py
--- 3.24.0-1/ase/test/fio/test_traj_bytesio.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_traj_bytesio.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import io
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/fio/test_trajectory.py 3.26.0-1/ase/test/fio/test_trajectory.py
--- 3.24.0-1/ase/test/fio/test_trajectory.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_trajectory.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atom, Atoms
diff -pruN 3.24.0-1/ase/test/fio/test_trajectory_heterogeneous.py 3.26.0-1/ase/test/fio/test_trajectory_heterogeneous.py
--- 3.24.0-1/ase/test/fio/test_trajectory_heterogeneous.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_trajectory_heterogeneous.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk, molecule
 from ase.constraints import FixAtoms, FixBondLength
 from ase.io import read
diff -pruN 3.24.0-1/ase/test/fio/test_turbomole.py 3.26.0-1/ase/test/fio/test_turbomole.py
--- 3.24.0-1/ase/test/fio/test_turbomole.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_turbomole.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import molecule
 from ase.constraints import FixAtoms
 from ase.io import read, write
diff -pruN 3.24.0-1/ase/test/fio/test_ulm.py 3.26.0-1/ase/test/fio/test_ulm.py
--- 3.24.0-1/ase/test/fio/test_ulm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_ulm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test ase.io.ulm file stuff."""
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/fio/test_ulm_dummy.py 3.26.0-1/ase/test/fio/test_ulm_dummy.py
--- 3.24.0-1/ase/test/fio/test_ulm_dummy.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_ulm_dummy.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test ase.io.ulm.DummyWriter."""
 import numpy as np
 
diff -pruN 3.24.0-1/ase/test/fio/test_v_sim.py 3.26.0-1/ase/test/fio/test_v_sim.py
--- 3.24.0-1/ase/test/fio/test_v_sim.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_v_sim.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Check reading of a sample v_sim .ascii file, and I/O consistency"""
 
 from ase.io import read
diff -pruN 3.24.0-1/ase/test/fio/test_vasp_structure.py 3.26.0-1/ase/test/fio/test_vasp_structure.py
--- 3.24.0-1/ase/test/fio/test_vasp_structure.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_vasp_structure.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,3 @@
-# type: ignore
 import io
 import os
 import unittest
@@ -112,15 +111,23 @@ indices_to_constrain = [0, 2]
 
 @pytest.fixture()
 def graphene_atoms():
-    atoms = graphene_nanoribbon(2, 2, type="armchair", saturated=False)
+    atoms = graphene_nanoribbon(2, 2, type='armchair', saturated=False)
     atoms.cell = [[10, 0, 0], [0, 10, 0], [0, 0, 10]]
     return atoms
 
 
 def poscar_roundtrip(atoms):
     """Write a POSCAR file, read it back and return the new atoms object"""
-    atoms.write("POSCAR", direct=True)
-    return ase.io.read("POSCAR")
+    atoms.write('POSCAR', direct=True)
+    return ase.io.read('POSCAR')
+
+
+@pytest.mark.parametrize('whitespace', ['\n', '   ', '   \n\n  \n'])
+def test_with_whitespace(graphene_atoms, whitespace):
+    graphene_atoms.write('POSCAR', direct=True)
+    with open('POSCAR', 'a') as fd:
+        fd.write(whitespace)
+    assert str(ase.io.read('POSCAR').symbols) == str(graphene_atoms.symbols)
 
 
 def test_FixAtoms(graphene_atoms):
@@ -157,3 +164,15 @@ def test_FixedLine_and_Plane(ConstraintC
     # or FixedPlane is along a lattice vector.
 
     assert np.all(constrained_indices(new_atoms) == indices_to_constrain)
+
+
+def test_write_read_velocities(graphene_atoms):
+    vel = np.zeros_like(graphene_atoms.positions)
+    vel = np.linspace(-1, 1, 3 * len(graphene_atoms)).reshape(-1, 3)
+    graphene_atoms.set_velocities(vel)
+
+    graphene_atoms.write('CONTCAR', direct=False)
+    new_atoms = ase.io.read('CONTCAR')
+    new_vel = new_atoms.get_velocities()
+
+    assert np.allclose(vel, new_vel)
diff -pruN 3.24.0-1/ase/test/fio/test_wout.py 3.26.0-1/ase/test/fio/test_wout.py
--- 3.24.0-1/ase/test/fio/test_wout.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_wout.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test Wannier90 wout format."""
 import io
 
diff -pruN 3.24.0-1/ase/test/fio/test_xsd_bond.py 3.26.0-1/ase/test/fio/test_xsd_bond.py
--- 3.24.0-1/ase/test/fio/test_xsd_bond.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_xsd_bond.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import re
 from collections import OrderedDict
 
diff -pruN 3.24.0-1/ase/test/fio/test_xsf_spec.py 3.26.0-1/ase/test/fio/test_xsf_spec.py
--- 3.24.0-1/ase/test/fio/test_xsf_spec.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_xsf_spec.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from pathlib import Path
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/fio/test_xyz.py 3.26.0-1/ase/test/fio/test_xyz.py
--- 3.24.0-1/ase/test/fio/test_xyz.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/test_xyz.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import filecmp
 
 import pytest
diff -pruN 3.24.0-1/ase/test/fio/vasp/test_incar_writer.py 3.26.0-1/ase/test/fio/vasp/test_incar_writer.py
--- 3.24.0-1/ase/test/fio/vasp/test_incar_writer.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/vasp/test_incar_writer.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from unittest.mock import mock_open, patch
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/fio/vasp/test_poscar.py 3.26.0-1/ase/test/fio/vasp/test_poscar.py
--- 3.24.0-1/ase/test/fio/vasp/test_poscar.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/vasp/test_poscar.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # import inspect
 from shutil import copyfile
 
diff -pruN 3.24.0-1/ase/test/fio/vasp/test_read_vasp_xml.py 3.26.0-1/ase/test/fio/vasp/test_read_vasp_xml.py
--- 3.24.0-1/ase/test/fio/vasp/test_read_vasp_xml.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/vasp/test_read_vasp_xml.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from collections import OrderedDict
 from io import StringIO
 from pathlib import Path
@@ -368,3 +369,43 @@ def test_constraints(vasprun):
 
     assert isinstance(atoms.constraints[1], FixAtoms)
     assert np.all(atoms.constraints[1].index == [1])
+
+
+def test_vasprun_line_mode(vasprun):
+    line_mode = """\
+ <kpoints>
+  <generation param="listgenerated">
+   <i name="divisions" type="int">       2 </i>
+   <v>       0.00000000       0.00000000       0.00000000 </v>
+   <v>       0.50000000       0.50000000       0.00000000 </v>
+   <v>       0.50000000       0.75000000       0.25000000 </v>
+   <v>       0.00000000       0.00000000       0.00000000 </v>
+  </generation>
+  <varray name="kpointlist" >
+   <v>       0.00000000       0.00000000       0.00000000 </v>
+   <v>       0.50000000       0.50000000       0.00000000 </v>
+   <v>       0.50000000       0.50000000       0.00000000 </v>
+   <v>       0.50000000       0.75000000       0.25000000 </v>
+   <v>       0.50000000       0.75000000       0.25000000 </v>
+   <v>       0.00000000       0.00000000       0.00000000 </v>
+  </varray>
+  <varray name="weights" >
+   <v>       0.16666667 </v>
+   <v>       0.16666667 </v>
+   <v>       0.16666667 </v>
+   <v>       0.16666667 </v>
+   <v>       0.16666667 </v>
+   <v>       0.16666667 </v>
+  </varray>
+  <kpoints_labels>
+   <i name="Γ" type="int">       1 </i>
+   <i name="X" type="int">       2 </i>
+   <i name="X" type="int">       3 </i>
+   <i name="W" type="int">       4 </i>
+   <i name="W" type="int">       5 </i>
+   <i name="Γ" type="int">       6 </i>
+  </kpoints_labels>
+ </kpoints>
+"""
+    assert read(StringIO(vasprun + line_mode),
+                format="vasp-xml")
diff -pruN 3.24.0-1/ase/test/fio/vasp/test_vasp_out.py 3.26.0-1/ase/test/fio/vasp/test_vasp_out.py
--- 3.24.0-1/ase/test/fio/vasp/test_vasp_out.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/vasp/test_vasp_out.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # flake8: noqa
 import inspect
 
diff -pruN 3.24.0-1/ase/test/fio/vasp/test_vasp_outcar_parsers.py 3.26.0-1/ase/test/fio/vasp/test_vasp_outcar_parsers.py
--- 3.24.0-1/ase/test/fio/vasp/test_vasp_outcar_parsers.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/vasp/test_vasp_outcar_parsers.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # flake8: noqa
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/fio/vasp/test_vasp_poscar.py 3.26.0-1/ase/test/fio/vasp/test_vasp_poscar.py
--- 3.24.0-1/ase/test/fio/vasp/test_vasp_poscar.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/fio/vasp/test_vasp_poscar.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import io
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/forcefields/test_acn.py 3.26.0-1/ase/test/forcefields/test_acn.py
--- 3.24.0-1/ase/test/forcefields/test_acn.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_acn.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.acn import ACN, m_me, r_cn, r_mec
 from ase.calculators.fd import calculate_numerical_forces
diff -pruN 3.24.0-1/ase/test/forcefields/test_aic.py 3.26.0-1/ase/test/forcefields/test_aic.py
--- 3.24.0-1/ase/test/forcefields/test_aic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_aic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/forcefields/test_combine_mm.py 3.26.0-1/ase/test/forcefields/test_combine_mm.py
--- 3.24.0-1/ase/test/forcefields/test_combine_mm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_combine_mm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import cos, pi, sin
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/forcefields/test_combine_mm2.py 3.26.0-1/ase/test/forcefields/test_combine_mm2.py
--- 3.24.0-1/ase/test/forcefields/test_combine_mm2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_combine_mm2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/forcefields/test_counterions.py 3.26.0-1/ase/test/forcefields/test_counterions.py
--- 3.24.0-1/ase/test/forcefields/test_counterions.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_counterions.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms, units
diff -pruN 3.24.0-1/ase/test/forcefields/test_forceqmmm.py 3.26.0-1/ase/test/forcefields/test_forceqmmm.py
--- 3.24.0-1/ase/test/forcefields/test_forceqmmm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_forceqmmm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/forcefields/test_qmmm.py 3.26.0-1/ase/test/forcefields/test_qmmm.py
--- 3.24.0-1/ase/test/forcefields/test_qmmm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_qmmm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import cos, pi, sin
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/forcefields/test_qmmm_acn.py 3.26.0-1/ase/test/forcefields/test_qmmm_acn.py
--- 3.24.0-1/ase/test/forcefields/test_qmmm_acn.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_qmmm_acn.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 import ase.units as units
diff -pruN 3.24.0-1/ase/test/forcefields/test_rattle.py 3.26.0-1/ase/test/forcefields/test_rattle.py
--- 3.24.0-1/ase/test/forcefields/test_rattle.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_rattle.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 import ase.units as units
diff -pruN 3.24.0-1/ase/test/forcefields/test_rattle_linear.py 3.26.0-1/ase/test/forcefields/test_rattle_linear.py
--- 3.24.0-1/ase/test/forcefields/test_rattle_linear.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_rattle_linear.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 import ase.units as units
diff -pruN 3.24.0-1/ase/test/forcefields/test_tip4p.py 3.26.0-1/ase/test/forcefields/test_tip4p.py
--- 3.24.0-1/ase/test/forcefields/test_tip4p.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_tip4p.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import cos, sin
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/forcefields/test_tipnp.py 3.26.0-1/ase/test/forcefields/test_tipnp.py
--- 3.24.0-1/ase/test/forcefields/test_tipnp.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_tipnp.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import cos, pi, sin
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/forcefields/test_utils_ff_vdw.py 3.26.0-1/ase/test/forcefields/test_utils_ff_vdw.py
--- 3.24.0-1/ase/test/forcefields/test_utils_ff_vdw.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/forcefields/test_utils_ff_vdw.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import math
 
 import pytest
diff -pruN 3.24.0-1/ase/test/ga/test_add_candidates.py 3.26.0-1/ase/test/ga/test_add_candidates.py
--- 3.24.0-1/ase/test/ga/test_add_candidates.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_add_candidates.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import fcc111
diff -pruN 3.24.0-1/ase/test/ga/test_basic_example_main_run.py 3.26.0-1/ase/test/ga/test_basic_example_main_run.py
--- 3.24.0-1/ase/test/ga/test_basic_example_main_run.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_basic_example_main_run.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/ga/test_bulk_operators.py 3.26.0-1/ase/test/ga/test_bulk_operators.py
--- 3.24.0-1/ase/test/ga/test_bulk_operators.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_bulk_operators.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/ga/test_chain_operators.py 3.26.0-1/ase/test/ga/test_chain_operators.py
--- 3.24.0-1/ase/test/ga/test_chain_operators.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_chain_operators.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/ga/test_create_database.py 3.26.0-1/ase/test/ga/test_create_database.py
--- 3.24.0-1/ase/test/ga/test_create_database.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_create_database.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/ga/test_cutandsplicepairing.py 3.26.0-1/ase/test/ga/test_cutandsplicepairing.py
--- 3.24.0-1/ase/test/ga/test_cutandsplicepairing.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_cutandsplicepairing.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import fcc111
diff -pruN 3.24.0-1/ase/test/ga/test_database_logic.py 3.26.0-1/ase/test/ga/test_database_logic.py
--- 3.24.0-1/ase/test/ga/test_database_logic.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_database_logic.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import fcc111
diff -pruN 3.24.0-1/ase/test/ga/test_element_operators.py 3.26.0-1/ase/test/ga/test_element_operators.py
--- 3.24.0-1/ase/test/ga/test_element_operators.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_element_operators.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/ga/test_film_operators.py 3.26.0-1/ase/test/ga/test_film_operators.py
--- 3.24.0-1/ase/test/ga/test_film_operators.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_film_operators.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/ga/test_mutations.py 3.26.0-1/ase/test/ga/test_mutations.py
--- 3.24.0-1/ase/test/ga/test_mutations.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_mutations.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import fcc111
diff -pruN 3.24.0-1/ase/test/ga/test_particle_comparators.py 3.26.0-1/ase/test/ga/test_particle_comparators.py
--- 3.24.0-1/ase/test/ga/test_particle_comparators.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_particle_comparators.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.cluster import Icosahedron
diff -pruN 3.24.0-1/ase/test/ga/test_particle_mutations.py 3.26.0-1/ase/test/ga/test_particle_mutations.py
--- 3.24.0-1/ase/test/ga/test_particle_mutations.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_particle_mutations.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import fcc111
diff -pruN 3.24.0-1/ase/test/ga/test_particle_operators.py 3.26.0-1/ase/test/ga/test_particle_operators.py
--- 3.24.0-1/ase/test/ga/test_particle_operators.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_particle_operators.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.cluster import Icosahedron
diff -pruN 3.24.0-1/ase/test/ga/test_slab_operators.py 3.26.0-1/ase/test/ga/test_slab_operators.py
--- 3.24.0-1/ase/test/ga/test_slab_operators.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_slab_operators.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/ga/test_standardcomparator.py 3.26.0-1/ase/test/ga/test_standardcomparator.py
--- 3.24.0-1/ase/test/ga/test_standardcomparator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/ga/test_standardcomparator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 from ase.calculators.singlepoint import SinglePointCalculator
 from ase.ga import set_raw_score
diff -pruN 3.24.0-1/ase/test/geometry/test_analysis.py 3.26.0-1/ase/test/geometry/test_analysis.py
--- 3.24.0-1/ase/test/geometry/test_analysis.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/geometry/test_analysis.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/geometry/test_dimensionality.py 3.26.0-1/ase/test/geometry/test_dimensionality.py
--- 3.24.0-1/ase/test/geometry/test_dimensionality.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/geometry/test_dimensionality.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 import ase.build
diff -pruN 3.24.0-1/ase/test/geometry/test_distance.py 3.26.0-1/ase/test/geometry/test_distance.py
--- 3.24.0-1/ase/test/geometry/test_distance.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/geometry/test_distance.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import itertools
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/geometry/test_geometry.py 3.26.0-1/ase/test/geometry/test_geometry.py
--- 3.24.0-1/ase/test/geometry/test_geometry.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/geometry/test_geometry.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from dataclasses import is_dataclass
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/geometry/test_geometry_derivatives.py 3.26.0-1/ase/test/geometry/test_geometry_derivatives.py
--- 3.24.0-1/ase/test/geometry/test_geometry_derivatives.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/geometry/test_geometry_derivatives.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/geometry/test_geometry_rdf.py 3.26.0-1/ase/test/geometry/test_geometry_rdf.py
--- 3.24.0-1/ase/test/geometry/test_geometry_rdf.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/geometry/test_geometry_rdf.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/gui/test_run.py 3.26.0-1/ase/test/gui/test_run.py
--- 3.24.0-1/ase/test/gui/test_run.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/gui/test_run.py	2025-08-12 11:26:23.000000000 +0000
@@ -59,6 +59,7 @@ def guifactory(display):
         gui = GUI(images)
         guis.append(gui)
         return gui
+
     yield factory
 
     for gui in guis:
@@ -154,6 +155,16 @@ def test_settings(gui):
     s.scale_radii()
 
 
+def test_magmom_arrows(gui):
+    gui.window['toggle-show-magmoms'] = True
+    gui.new_atoms(molecule('O2'))
+    s = gui.settings()
+    gui.magmom_vector_scale = 0.5
+    s.magmom_vector_scale.value = 2.1
+    s.scale_magmom_vectors()
+    assert gui.magmom_vector_scale == pytest.approx(2.1)
+
+
 def test_rotate(gui):
     gui.window['toggle-show-bonds'] = True
     gui.new_atoms(molecule('H2O'))
@@ -168,10 +179,17 @@ def test_open_and_save(gui, testdir):
     save_dialog(gui, 'h2o.cif@-1')
 
 
-@pytest.mark.parametrize('filename', [
-    None, 'output.png', 'output.eps',
-    'output.pov', 'output.traj', 'output.traj@0',
-])
+@pytest.mark.parametrize(
+    'filename',
+    [
+        None,
+        'output.png',
+        'output.eps',
+        'output.pov',
+        'output.traj',
+        'output.traj@0',
+    ],
+)
 def test_export_graphics(gui, testdir, with_bulk_ti, monkeypatch, filename):
     # Monkeypatch the blocking dialog:
     monkeypatch.setattr(ui.SaveFileDialog, 'go', lambda event: filename)
@@ -231,6 +249,7 @@ def test_select_atoms(gui, with_bulk_ti)
 def test_modify_element(gui, modify):
     class MockElement:
         Z = 79
+
     modify.set_element(MockElement())
     assert all(gui.atoms.symbols[:4] == 'Au')
     assert all(gui.atoms.symbols[4:] == 'Ti')
@@ -326,6 +345,7 @@ def test_reciprocal(gui):
     reciprocal = gui.reciprocal()
     reciprocal.terminate()
     exitcode = reciprocal.wait(timeout=5)
+    reciprocal.stdout.close()
     assert exitcode != 0
 
 
@@ -442,11 +462,39 @@ def test_clipboard_paste_onto_existing(g
     assert gui.atoms == ti + h2o
 
 
-@pytest.mark.parametrize('text', [
-    '',
-    'invalid_atoms',
-    '[1, 2, 3]',  # valid JSON but not Atoms
-])
+def test_wrap(gui):
+    """Test the Wrap atoms function."""
+    atoms = bulk('Si')
+    atoms.positions += 1234
+    gui.new_atoms(atoms)
+    unwrapped = atoms.get_scaled_positions(wrap=False)
+    wrapped_ref = atoms.get_scaled_positions(wrap=True)
+
+    assert (unwrapped > 1).all()
+    gui.wrap_atoms()
+    wrapped = gui.images[0].get_scaled_positions(wrap=False)
+    assert (wrapped < 1).all()
+    assert (wrapped >= 0).all()
+    assert wrapped == pytest.approx(wrapped_ref)
+
+
+def test_show_labels(gui):
+    atoms = molecule('CH3CH2OH')
+    gui.new_atoms(atoms)
+    assert gui.get_labels() is None
+    gui.window['show-labels'] = 3  # ugly: magical code for chemical symbols
+    gui.draw()
+    assert list(gui.get_labels()) == list(atoms.symbols)
+
+
+@pytest.mark.parametrize(
+    'text',
+    [
+        '',
+        'invalid_atoms',
+        '[1, 2, 3]',  # valid JSON but not Atoms
+    ],
+)
 def test_clipboard_paste_invalid(gui, text):
     gui.clipboard.set_text(text)
     with pytest.raises(GUIError):
@@ -454,12 +502,13 @@ def test_clipboard_paste_invalid(gui, te
 
 
 def window():
-
     def hello(event=None):
         print('hello', event)
 
-    menu = [('Hi', [ui.MenuItem('_Hello', hello, 'Ctrl+H')]),
-            ('Hell_o', [ui.MenuItem('ABC', hello, choices='ABC')])]
+    menu = [
+        ('Hi', [ui.MenuItem('_Hello', hello, 'Ctrl+H')]),
+        ('Hell_o', [ui.MenuItem('ABC', hello, choices='ABC')]),
+    ]
     win = ui.MainWindow('Test', menu=menu)
 
     win.add(ui.Label('Hello'))
@@ -499,3 +548,58 @@ def runcallbacks(win):
 def test_callbacks(display):
     win = window()
     win.win.after_idle(runcallbacks)
+
+
+def test_atoms_editor_set_values(gui, atoms):
+    editor = gui.atoms_editor()
+
+    assert str(atoms.symbols) == 'Ti16'
+    entry, apply_change = editor.edit_field(row_id='R3', column_id='#1')
+    entry.delete(0, 'end')
+    entry.insert(0, 'Pu')
+    apply_change()
+
+    assert str(atoms.symbols) == 'Ti3PuTi12'
+
+    for i in range(3):
+        # Edit each coordinate:
+        entry, apply_change = editor.edit_field('R4', f'#{2 + i}')
+        entry.delete(0, 'end')
+        value = str(5.1 + i)
+        entry.insert(0, value)
+        apply_change()
+
+    assert atoms.positions[4] == pytest.approx([5.1, 6.1, 7.1])
+
+
+def test_atoms_editor_change_listener(gui, atoms):
+    editor = gui.atoms_editor()
+    entry, _ = editor.edit_field('R2', '#1')
+    assert entry.get() == 'Ti'
+    editor.leave_edit_mode()
+
+    atoms = molecule('CH3CH2OH')
+    gui.new_atoms(atoms)
+    entry, _ = editor.edit_field('R2', '#1')
+    assert entry.get() == 'O'
+
+
+def test_atoms_editor_select_in_gui(gui, atoms):
+    """Test that contents of editor updates when atoms change."""
+    editor = gui.atoms_editor()
+    assert sum(gui.images.selected) == 0
+    assert len(editor.treeview.selection()) == 0
+
+    gui.set_selected_atoms([2, 5, 6])
+    selection = editor.treeview.selection()
+    assert selection == ('R2', 'R5', 'R6')
+
+
+def test_atoms_editor_select_in_editor(gui, atoms):
+    """Test that GUI selection changes when editor selection does."""
+    editor = gui.atoms_editor()
+    editor.treeview.selection_set('R6', 'R7', 'R8', 'R10')
+    editor.treeview.event_generate('<<TreeviewSelect>>')
+    print(gui.images.selected)
+    assert all(gui.images.selected[[6, 7, 8, 10]])
+    assert sum(gui.images.selected) == 4
diff -pruN 3.24.0-1/ase/test/lammpsdata/comparison.py 3.26.0-1/ase/test/lammpsdata/comparison.py
--- 3.24.0-1/ase/test/lammpsdata/comparison.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/lammpsdata/comparison.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/lammpsdata/conftest.py 3.26.0-1/ase/test/lammpsdata/conftest.py
--- 3.24.0-1/ase/test/lammpsdata/conftest.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/lammpsdata/conftest.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/lammpsdata/parse_lammps_data_file.py 3.26.0-1/ase/test/lammpsdata/parse_lammps_data_file.py
--- 3.24.0-1/ase/test/lammpsdata/parse_lammps_data_file.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/lammpsdata/parse_lammps_data_file.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 Routines for manually parsing a lammps data file.  This is a simplified
 recreation of ase.io.lammpsdata's read functionality that we use for
diff -pruN 3.24.0-1/ase/test/lammpsdata/test_lammpsdata_read.py 3.26.0-1/ase/test/lammpsdata/test_lammpsdata_read.py
--- 3.24.0-1/ase/test/lammpsdata/test_lammpsdata_read.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/lammpsdata/test_lammpsdata_read.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 Use lammpsdata module to create an Atoms object from a lammps data file
 and checks that the cell, mass, positions, and velocities match the
diff -pruN 3.24.0-1/ase/test/lammpsdata/test_lammpsdata_write.py 3.26.0-1/ase/test/lammpsdata/test_lammpsdata_write.py
--- 3.24.0-1/ase/test/lammpsdata/test_lammpsdata_write.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/lammpsdata/test_lammpsdata_write.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 Create an atoms object and write it to a lammps data file
 """
diff -pruN 3.24.0-1/ase/test/lammpsdata/test_read_connectivity.py 3.26.0-1/ase/test/lammpsdata/test_read_connectivity.py
--- 3.24.0-1/ase/test/lammpsdata/test_read_connectivity.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/lammpsdata/test_read_connectivity.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Testing lammpsdata reader."""
 
 import re
diff -pruN 3.24.0-1/ase/test/lammpsdata/test_write_and_read.py 3.26.0-1/ase/test/lammpsdata/test_write_and_read.py
--- 3.24.0-1/ase/test/lammpsdata/test_write_and_read.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/lammpsdata/test_write_and_read.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test write and read."""
 import io
 import re
diff -pruN 3.24.0-1/ase/test/md/test_CO2linear_Au111_langevin.py 3.26.0-1/ase/test/md/test_CO2linear_Au111_langevin.py
--- 3.24.0-1/ase/test/md/test_CO2linear_Au111_langevin.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_CO2linear_Au111_langevin.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import cos, pi, sin
 
 import numpy as np
@@ -17,7 +18,7 @@ def test_CO2linear_Au111_langevin(testdi
     triatomic molecules"""
 
     rng = np.random.RandomState(0)
-    eref = 3.131939
+    eref = 3.148932
 
     zpos = cos(134.3 / 2.0 * pi / 180.0) * 1.197
     xpos = sin(134.3 / 2.0 * pi / 180.0) * 1.19
diff -pruN 3.24.0-1/ase/test/md/test_bussi.py 3.26.0-1/ase/test/md/test_bussi.py
--- 3.24.0-1/ase/test/md/test_bussi.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_bussi.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/md/test_ce_curvature.py 3.26.0-1/ase/test/md/test_ce_curvature.py
--- 3.24.0-1/ase/test/md/test_ce_curvature.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_ce_curvature.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 '''These tests ensure that the computed PEC curvature matche the actual
 geometries using a somewhat agressive angle_limit for each stepsize.'''
 import numpy as np
diff -pruN 3.24.0-1/ase/test/md/test_ce_logging.py 3.26.0-1/ase/test/md/test_ce_logging.py
--- 3.24.0-1/ase/test/md/test_ce_logging.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_ce_logging.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """This test ensures that logging to a text file and to the trajectory file are
 reporting the same values as in the ContourExploration object."""
 
diff -pruN 3.24.0-1/ase/test/md/test_ce_potentiostat.py 3.26.0-1/ase/test/md/test_ce_potentiostat.py
--- 3.24.0-1/ase/test/md/test_ce_potentiostat.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_ce_potentiostat.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 '''These tests ensure that the potentiostat can keep a sysytem near the PEC'''
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/md/test_fixrotation.py 3.26.0-1/ase/test/md/test_fixrotation.py
--- 3.24.0-1/ase/test/md/test_fixrotation.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_fixrotation.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/md/test_idealgas.py 3.26.0-1/ase/test/md/test_idealgas.py
--- 3.24.0-1/ase/test/md/test_idealgas.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_idealgas.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/md/test_langevin_asapcompat.py 3.26.0-1/ase/test/md/test_langevin_asapcompat.py
--- 3.24.0-1/ase/test/md/test_langevin_asapcompat.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_langevin_asapcompat.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import units
 from ase.build import bulk
 from ase.calculators.emt import EMT
diff -pruN 3.24.0-1/ase/test/md/test_langevin_com.py 3.26.0-1/ase/test/md/test_langevin_com.py
--- 3.24.0-1/ase/test/md/test_langevin_com.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_langevin_com.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import units
diff -pruN 3.24.0-1/ase/test/md/test_langevin_switching.py 3.26.0-1/ase/test/md/test_langevin_switching.py
--- 3.24.0-1/ase/test/md/test_langevin_switching.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_langevin_switching.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
@@ -19,7 +20,7 @@ def test_langevin_switching():
     dt = 10
 
     # for reproducibility
-    np.random.seed(42)
+    rng = np.random.RandomState(42)
 
     # setup atoms and calculators
     atoms = bulk('Al').repeat(size)
@@ -37,16 +38,18 @@ def test_langevin_switching():
     # switch_forward
     with SwitchLangevin(atoms, calc1, calc2, dt * units.fs,
                         temperature_K=T, friction=0.01,
-                        n_eq=n_steps, n_switch=n_steps) as dyn_forward:
-        MaxwellBoltzmannDistribution(atoms, temperature_K=2 * T)
+                        n_eq=n_steps, n_switch=n_steps,
+                        rng=rng) as dyn_forward:
+        MaxwellBoltzmannDistribution(atoms, temperature_K=2 * T, rng=rng)
         dyn_forward.run()
         dF_forward = dyn_forward.get_free_energy_difference() / len(atoms)
 
     # switch_backwards
     with SwitchLangevin(atoms, calc2, calc1, dt * units.fs,
                         temperature_K=T, friction=0.01,
-                        n_eq=n_steps, n_switch=n_steps) as dyn_backward:
-        MaxwellBoltzmannDistribution(atoms, temperature_K=2 * T)
+                        n_eq=n_steps, n_switch=n_steps,
+                        rng=rng) as dyn_backward:
+        MaxwellBoltzmannDistribution(atoms, temperature_K=2 * T, rng=rng)
         dyn_backward.run()
         dF_backward = -dyn_backward.get_free_energy_difference() / len(atoms)
 
diff -pruN 3.24.0-1/ase/test/md/test_maxwellboltzmann.py 3.26.0-1/ase/test/md/test_maxwellboltzmann.py
--- 3.24.0-1/ase/test/md/test_maxwellboltzmann.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_maxwellboltzmann.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.constraints import FixAtoms
diff -pruN 3.24.0-1/ase/test/md/test_md.py 3.26.0-1/ase/test/md/test_md.py
--- 3.24.0-1/ase/test/md/test_md.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_md.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import units
diff -pruN 3.24.0-1/ase/test/md/test_md_logger.py 3.26.0-1/ase/test/md/test_md_logger.py
--- 3.24.0-1/ase/test/md/test_md_logger.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_md_logger.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test to ensure that md logger and trajectory contain same data"""
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/md/test_md_logger_interval.py 3.26.0-1/ase/test/md/test_md_logger_interval.py
--- 3.24.0-1/ase/test/md/test_md_logger_interval.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_md_logger_interval.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/md/test_md_stepwise.py 3.26.0-1/ase/test/md/test_md_stepwise.py
--- 3.24.0-1/ase/test/md/test_md_stepwise.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_md_stepwise.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,36 @@
+"""Tests for `MolecularDynmics.irun`."""
+
+import pytest
+
+from ase.atoms import Atoms
+from ase.build import bulk
+from ase.calculators.calculator import compare_atoms
+from ase.calculators.emt import EMT
+from ase.md.npt import NPT
+from ase.units import fs
+
+
+@pytest.fixture(name='atoms0')
+def fixture_atoms0():
+    """Make `atoms0`."""
+    atoms = bulk('Au', cubic=True)
+    atoms.rattle(stdev=0.15)
+    return atoms
+
+
+@pytest.fixture(name='atoms')
+def fixture_atoms(atoms0):
+    """Make `atoms`."""
+    atoms = atoms0.copy()
+    atoms.calc = EMT()
+    return atoms
+
+
+def test_irun_start(atoms0: Atoms, atoms: Atoms) -> None:
+    """Test if `irun` works."""
+    with NPT(atoms, timestep=1.0 * fs, temperature_K=1000.0) as md:
+        irun = md.irun(steps=10)
+        next(irun)  # Initially it yields without yet having performed a step:
+        assert not compare_atoms(atoms0, atoms)
+        next(irun)  # Now it must have performed a step:
+        assert compare_atoms(atoms0, atoms) == ['positions']
diff -pruN 3.24.0-1/ase/test/md/test_nose_hoover_chain.py 3.26.0-1/ase/test/md/test_nose_hoover_chain.py
--- 3.24.0-1/ase/test/md/test_nose_hoover_chain.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_nose_hoover_chain.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,46 +1,219 @@
+# fmt: off
 from __future__ import annotations
 
+from copy import deepcopy
+
 import numpy as np
 import pytest
 
 import ase.build
 import ase.units
+from ase import Atoms
 from ase.md.nose_hoover_chain import (
+    MTKNPT,
+    IsotropicMTKBarostat,
+    IsotropicMTKNPT,
+    MTKBarostat,
     NoseHooverChainNVT,
     NoseHooverChainThermostat,
 )
 from ase.md.velocitydistribution import MaxwellBoltzmannDistribution, Stationary
 
 
-@pytest.mark.parametrize("tchain", [1, 3])
-def test_thermostat(tchain: int):
+@pytest.fixture
+def hcp_Cu() -> Atoms:
     atoms = ase.build.bulk(
         "Cu", crystalstructure='hcp', a=2.53, c=4.11
     ).repeat(2)
+    return atoms
+
+
+@pytest.mark.parametrize("tchain", [1, 3])
+@pytest.mark.parametrize("tloop", [1, 3])
+def test_thermostat_round_trip(hcp_Cu: Atoms, tchain: int, tloop: int):
+    atoms = hcp_Cu.copy()
 
     timestep = 1.0 * ase.units.fs
     thermostat = NoseHooverChainThermostat(
+        num_atoms_global=len(atoms),
         masses=atoms.get_masses()[:, None],
         temperature_K=1000,
         tdamp=100 * timestep,
         tchain=tchain,
+        tloop=tloop,
     )
 
     rng = np.random.default_rng(0)
     p = rng.standard_normal(size=(len(atoms), 3))
 
+    # Forward `n` steps and backward `n` steps with`, which should go back to
+    # the initial state.
     n = 1000
     p_start = p.copy()
     eta_start = thermostat._eta.copy()
     p_eta_start = thermostat._p_eta.copy()
+
     for _ in range(n):
         p = thermostat.integrate_nhc(p, timestep)
-    for _ in range(2 * n):
-        p = thermostat.integrate_nhc(p, -0.5 * timestep)
+    assert not np.allclose(p, p_start, atol=1e-6)
+    assert not np.allclose(thermostat._eta, eta_start, atol=1e-6)
+    assert not np.allclose(thermostat._p_eta, p_eta_start, atol=1e-6)
 
+    for _ in range(n):
+        p = thermostat.integrate_nhc(p, -timestep)
     assert np.allclose(p, p_start, atol=1e-6)
-    assert np.allclose(thermostat._eta, eta_start, atol=1e-6)
-    assert np.allclose(thermostat._p_eta, p_eta_start, atol=1e-6)
+
+    # These values are apparently very machine-dependent:
+    assert np.allclose(thermostat._eta, eta_start, atol=1e-5)
+    assert np.allclose(thermostat._p_eta, p_eta_start, atol=1e-4)
+
+
+@pytest.mark.parametrize("tchain", [1, 3])
+@pytest.mark.parametrize("tloop", [1, 3])
+def test_thermostat_truncation_error(hcp_Cu: Atoms, tchain: int, tloop: int):
+    """Compare thermostat integration with delta by n steps and delta/2 by 2n
+    steps. The difference between the two results should decrease with delta
+    until reaching rounding error.
+    """
+    atoms = hcp_Cu.copy()
+
+    delta0 = 1e-1
+    n = 100
+    m = 10
+
+    list_p_diff = []
+    list_eta_diff = []
+    list_p_eta_diff = []
+    for i in range(m):
+        delta = delta0 * (2 ** -i)
+        thermostat = NoseHooverChainThermostat(
+            num_atoms_global=len(atoms),
+            masses=atoms.get_masses()[:, None],
+            temperature_K=1000,
+            tdamp=100 * delta0,
+            tchain=tchain,
+            tloop=tloop,
+        )
+
+        rng = np.random.default_rng(0)
+        p = rng.standard_normal(size=(len(atoms), 3))
+        thermostat._eta = rng.standard_normal(size=(tchain, ))
+        thermostat._p_eta = rng.standard_normal(size=(tchain, ))
+
+        thermostat1 = deepcopy(thermostat)
+        p1 = p.copy()
+        for _ in range(n):
+            p1 = thermostat1.integrate_nhc(p1, delta)
+
+        thermostat2 = deepcopy(thermostat)
+        p2 = p.copy()
+        for _ in range(2 * n):
+            p2 = thermostat2.integrate_nhc(p2, delta / 2)
+
+        # O(delta^3) truncation error
+        list_p_diff.append(np.linalg.norm(p1 - p2))
+        list_eta_diff.append(
+            np.linalg.norm(thermostat1._eta - thermostat2._eta)
+        )
+        list_p_eta_diff.append(
+            np.linalg.norm(thermostat1._p_eta - thermostat2._p_eta)
+        )
+
+    print(np.array(list_p_diff))
+    print(np.array(list_eta_diff))
+    print(np.array(list_p_eta_diff))
+
+    # Check that the differences decrease with delta until reaching rounding
+    # error.
+    eps = 1e-12
+    for i in range(1, m):
+        assert (
+            (list_p_diff[i] < eps)
+            or (list_p_diff[i] < list_p_diff[i - 1])
+        )
+        assert (
+            (list_eta_diff[i] < eps)
+            or (list_eta_diff[i] < list_eta_diff[i - 1])
+        )
+        assert (
+            (list_p_eta_diff[i] < eps)
+            or (list_p_eta_diff[i] < list_p_eta_diff[i - 1])
+        )
+
+
+@pytest.mark.parametrize("pchain", [1, 3])
+@pytest.mark.parametrize("ploop", [1, 3])
+def test_isotropic_barostat(asap3, hcp_Cu: Atoms, pchain: int, ploop: int):
+    atoms = hcp_Cu.copy()
+    atoms.calc = asap3.EMT()
+
+    timestep = 1.0 * ase.units.fs
+    barostat = IsotropicMTKBarostat(
+        num_atoms_global=len(atoms),
+        temperature_K=1000,
+        pdamp=1000 * timestep,
+        pchain=pchain,
+        ploop=ploop,
+    )
+
+    rng = np.random.default_rng(0)
+    p_eps = float(rng.standard_normal())
+
+    # Forward `n` steps and backward `n` steps with`, which should go back to
+    # the initial state.
+    n = 1000
+    p_eps_start = p_eps
+    xi_start = barostat._xi.copy()
+    p_xi_start = barostat._p_xi.copy()
+    for _ in range(n):
+        p_eps = barostat.integrate_nhc_baro(p_eps, timestep)
+    assert not np.allclose(p_eps, p_eps_start, atol=1e-6)
+    assert not np.allclose(barostat._xi, xi_start, atol=1e-6)
+    assert not np.allclose(barostat._p_xi, p_xi_start, atol=1e-6)
+
+    for _ in range(n):
+        p_eps = barostat.integrate_nhc_baro(p_eps, -timestep)
+    assert np.allclose(p_eps, p_eps_start, atol=1e-6)
+    assert np.allclose(barostat._xi, xi_start, atol=1e-6)
+    assert np.allclose(barostat._p_xi, p_xi_start, atol=1e-6)
+
+
+@pytest.mark.parametrize("pchain", [1, 3])
+@pytest.mark.parametrize("ploop", [1, 3])
+def test_anisotropic_barostat(asap3, hcp_Cu: Atoms, pchain: int, ploop: int):
+    atoms = hcp_Cu.copy()
+    atoms.calc = asap3.EMT()
+
+    timestep = 1.0 * ase.units.fs
+    barostat = MTKBarostat(
+        num_atoms_global=len(atoms),
+        temperature_K=1000,
+        pdamp=1000 * timestep,
+        pchain=pchain,
+    )
+
+    rng = np.random.default_rng(0)
+    p_g = rng.standard_normal((3, 3))
+    p_g = 0.5 * (p_g + p_g.T)
+
+    n = 1000
+    p_g_start = p_g.copy()
+    xi_start = barostat._xi.copy()
+    p_xi_start = barostat._p_xi.copy()
+
+    for _ in range(n):
+        p_g = barostat.integrate_nhc_baro(p_g, timestep)
+    # extended variables should be updated by n * timestep
+    assert not np.allclose(p_g, p_g_start, atol=1e-6)
+    assert not np.allclose(barostat._xi, xi_start, atol=1e-6)
+    assert not np.allclose(barostat._p_xi, p_xi_start, atol=1e-6)
+
+    for _ in range(n):
+        p_g = barostat.integrate_nhc_baro(p_g, -timestep)
+    # Now, the extended variables should be back to the initial state
+    assert np.allclose(p_g, p_g_start, atol=1e-6)
+    assert np.allclose(barostat._xi, xi_start, atol=1e-6)
+    assert np.allclose(barostat._p_xi, p_xi_start, atol=1e-6)
 
 
 @pytest.mark.parametrize("tchain", [1, 3])
@@ -69,3 +242,68 @@ def test_nose_hoover_chain_nvt(asap3, tc
     conserved_energy2 = md.get_conserved_energy()
     assert np.allclose(np.sum(atoms.get_momenta(), axis=0), 0.0)
     assert np.isclose(conserved_energy1, conserved_energy2, atol=1e-3)
+
+
+@pytest.mark.parametrize("tchain", [1, 3])
+@pytest.mark.parametrize("pchain", [1, 3])
+def test_isotropic_mtk_npt(asap3, hcp_Cu: Atoms, tchain: int, pchain: int):
+    atoms = hcp_Cu.copy()
+    atoms.calc = asap3.EMT()
+
+    temperature_K = 300
+    rng = np.random.default_rng(0)
+    MaxwellBoltzmannDistribution(
+        atoms,
+        temperature_K=temperature_K, force_temp=True, rng=rng
+    )
+    Stationary(atoms)
+
+    timestep = 1.0 * ase.units.fs
+    md = IsotropicMTKNPT(
+        atoms,
+        timestep=timestep,
+        temperature_K=temperature_K,
+        pressure_au=10.0 * ase.units.GPa,
+        tdamp=100 * timestep,
+        pdamp=1000 * timestep,
+        tchain=tchain,
+        pchain=pchain,
+    )
+
+    conserved_energy1 = md.get_conserved_energy()
+    md.run(100)
+    conserved_energy2 = md.get_conserved_energy()
+    assert np.allclose(np.sum(atoms.get_momenta(), axis=0), 0.0)
+    assert np.isclose(conserved_energy1, conserved_energy2, atol=1e-3)
+
+
+@pytest.mark.parametrize("tchain", [1, 3])
+@pytest.mark.parametrize("pchain", [1, 3])
+def test_anisotropic_npt(asap3, hcp_Cu: Atoms, tchain: int, pchain: int):
+    atoms = hcp_Cu.copy()
+    atoms.calc = asap3.EMT()
+
+    temperature_K = 300
+    rng = np.random.default_rng(0)
+    MaxwellBoltzmannDistribution(
+        atoms,
+        temperature_K=temperature_K, force_temp=True, rng=rng
+    )
+    Stationary(atoms)
+
+    timestep = 1.0 * ase.units.fs
+    md = MTKNPT(
+        atoms,
+        timestep=timestep,
+        temperature_K=temperature_K,
+        pressure_au=10.0 * ase.units.GPa,
+        tdamp=100 * timestep,
+        pdamp=1000 * timestep,
+    )
+    conserved_energy1 = md.get_conserved_energy()
+    positions1 = atoms.get_positions().copy()
+    md.run(100)
+    conserved_energy2 = md.get_conserved_energy()
+    assert np.allclose(np.sum(atoms.get_momenta(), axis=0), 0.0)
+    assert np.isclose(conserved_energy1, conserved_energy2, atol=1e-3)
+    assert not np.allclose(atoms.get_positions(), positions1, atol=1e-6)
diff -pruN 3.24.0-1/ase/test/md/test_nvt_npt.py 3.26.0-1/ase/test/md/test_nvt_npt.py
--- 3.24.0-1/ase/test/md/test_nvt_npt.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_nvt_npt.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,8 +1,12 @@
+# fmt: off
 import numpy as np
 import pytest
 
 from ase import Atoms
-from ase.build import bulk
+from ase.build import bulk, make_supercell
+from ase.md.bussi import Bussi
+from ase.md.langevin import Langevin
+from ase.md.nose_hoover_chain import NoseHooverChainNVT
 from ase.md.npt import NPT
 from ase.md.nptberendsen import NPTBerendsen
 from ase.md.nvtberendsen import NVTBerendsen
@@ -11,30 +15,42 @@ from ase.units import GPa, bar, fs
 
 
 @pytest.fixture(scope='module')
-def berendsenparams():
-    """Parameters for the two Berendsen algorithms."""
+def dynamicsparams():
+    """Parameters for the Dynamics."""
     Bgold = 220.0 * GPa  # Bulk modulus of gold, in bar (1 GPa = 10000 bar)
-    nvtparam = dict(temperature_K=300, taut=1000 * fs)
-    nptparam = dict(temperature_K=300, pressure_au=5000 * bar, taut=1000 * fs,
-                    taup=1000 * fs,
-                    compressibility_au=1 / Bgold)
-    return dict(nvt=nvtparam, npt=nptparam)
+    taut = 1000 * fs
+    taup = 1000 * fs
+    nvtparam = dict(temperature_K=300, taut=taut)
+    nptparam = dict(temperature_K=300, pressure_au=5000 * bar, taut=taut,
+                    taup=taup, compressibility_au=1 / Bgold)
+    langevinparam = dict(temperature_K=300, friction=1 / (2 * taut))
+    nhparam = dict(temperature_K=300, tdamp=taut)
+    # NPT uses different units.  The factor 1.3 is the bulk modulus of gold in
+    # ev/Å^3
+    nptoldparam = dict(temperature_K=300, ttime=taut,
+                       externalstress=5000 * bar,
+                       pfactor=taup**2 * 1.3)
+    return dict(
+        nvt=nvtparam,
+        npt=nptparam,
+        langevin=langevinparam,
+        nosehoover=nhparam,
+        nptold=nptoldparam
+        )
 
 
-@pytest.fixture(scope='module')
-def equilibrated(asap3, berendsenparams):
+def equilibrate(atoms, dynamicsparams):
     """Make an atomic system with equilibrated temperature and pressure."""
     rng = np.random.RandomState(42)
-    # Must be big enough to avoid ridiculous fluctuations
-    atoms = bulk('Au', cubic=True).repeat((3, 3, 3))
-    atoms.calc = asap3.EMT()
-    MaxwellBoltzmannDistribution(atoms, temperature_K=100, force_temp=True,
+    # Must be small enough that we can see the an off-by-one error
+    # in the energy
+    MaxwellBoltzmannDistribution(atoms, temperature_K=300, force_temp=True,
                                  rng=rng)
     Stationary(atoms)
-    assert abs(atoms.get_temperature() - 100) < 0.0001
+    assert abs(atoms.get_temperature() - 300) < 0.0001
     with NPTBerendsen(atoms, timestep=20 * fs, logfile='-',
                       loginterval=200,
-                      **berendsenparams['npt']) as md:
+                      **dynamicsparams['npt']) as md:
         # Equilibrate for 20 ps
         md.run(steps=1000)
     T = atoms.get_temperature()
@@ -44,7 +60,41 @@ def equilibrated(asap3, berendsenparams)
     return atoms
 
 
-def propagate(atoms, asap3, algorithm, algoargs):
+@pytest.fixture(scope='module')
+def equilibrated(asap3, dynamicsparams):
+    atoms = bulk('Au', cubic=True)
+    atoms.calc = asap3.EMT()
+
+    return equilibrate(atoms, dynamicsparams)
+
+
+@pytest.fixture(scope='module')
+def equilibrated_upper_tri(asap3, dynamicsparams):
+    atoms = make_supercell(bulk('Pt', cubic=True),
+                           [[1, 1, 0], [0, 1, 1], [0, 0, 1]])
+    atoms.calc = asap3.EMT()
+    return equilibrate(atoms, dynamicsparams)
+
+
+@pytest.fixture(scope='module')
+def equilibrated_lower_tri(asap3, dynamicsparams):
+    atoms = bulk('Pt') * (3, 1, 1)
+    atoms.calc = asap3.EMT()
+
+    # Rotate to lower triangular cell matrix
+    atoms.set_cell(atoms.cell.standard_form()[0], scale_atoms=True)
+
+    return equilibrate(atoms, dynamicsparams)
+
+
+def propagate(atoms,
+              asap3,
+              algorithm,
+              algoargs,
+              max_pressure_error=None,
+              com_not_thermalized=False
+    ):
+    print(f'Propagating algorithm in {str(algorithm)}.')
     T = []
     p = []
     with algorithm(
@@ -53,8 +103,8 @@ def propagate(atoms, asap3, algorithm, a
             logfile='-',
             loginterval=1000,
             **algoargs) as md:
-        # Gather data for 50 ps
-        for _ in range(500):
+        # Gather 2000 data points for decent statistics
+        for _ in range(2000):
             md.run(5)
             T.append(atoms.get_temperature())
             pres = - atoms.get_stress(include_ideal_gas=True)[:3].sum() / 3
@@ -68,41 +118,90 @@ def propagate(atoms, asap3, algorithm, a
         Tmean * len(atoms) / (len(atoms) - 1)))
     print('Pressure: {:.2f} bar +/- {:.2f} bar  (N={})'.format(
         pmean / bar, np.std(p) / bar, len(p)))
-    return Tmean, pmean
+    # Temperature error: We should be able to detect a error of 1/N_atoms
+    # The factor .67 is arbitrary, smaller than 1.0 so we consistently
+    # detect errors, but not so small that we get false positives.
+    maxtemperr = 0.67 * 1 / len(atoms)
+    targettemp = algoargs['temperature_K']
+    if com_not_thermalized:
+        targettemp *= (len(atoms) - 1) / len(atoms)
+    assert abs(Tmean - targettemp) < maxtemperr * targettemp
+    if max_pressure_error:
+        try:
+            # Different algorithms use different keywords
+            targetpressure = algoargs['pressure_au']
+        except KeyError:
+            targetpressure = algoargs['externalstress']
+        assert abs(pmean - targetpressure) < max_pressure_error
 
 
 # Not a real optimizer test but uses optimizers.
 # We should probably not mark this (in general)
 @pytest.mark.optimize()
 @pytest.mark.slow()
-def test_nvtberendsen(asap3, equilibrated, berendsenparams, allraise):
-    t, _ = propagate(Atoms(equilibrated), asap3,
-                     NVTBerendsen, berendsenparams['nvt'])
-    assert abs(t - berendsenparams['nvt']['temperature_K']) < 0.5
+def test_nvtberendsen(asap3, equilibrated, dynamicsparams, allraise):
+    propagate(Atoms(equilibrated), asap3,
+              NVTBerendsen, dynamicsparams['nvt'])
 
 
 @pytest.mark.optimize()
 @pytest.mark.slow()
-def test_nptberendsen(asap3, equilibrated, berendsenparams, allraise):
-    t, p = propagate(Atoms(equilibrated), asap3,
-                     NPTBerendsen, berendsenparams['npt'])
-    assert abs(t - berendsenparams['npt']['temperature_K']) < 1.0
-    assert abs(p - berendsenparams['npt']['pressure_au']) < 25.0 * bar
+def test_langevin(asap3, equilibrated, dynamicsparams, allraise):
+    propagate(Atoms(equilibrated), asap3,
+              Langevin, dynamicsparams['langevin'])
 
 
 @pytest.mark.optimize()
 @pytest.mark.slow()
-def test_npt(asap3, equilibrated, berendsenparams, allraise):
-    params = berendsenparams['npt']
-    # NPT uses different units.  The factor 1.3 is the bulk modulus of gold in
-    # ev/Å^3
-    t, p = propagate(Atoms(equilibrated), asap3, NPT,
-                     dict(temperature_K=params['temperature_K'],
-                          externalstress=params['pressure_au'],
-                          ttime=params['taut'],
-                          pfactor=params['taup']**2 * 1.3))
+def test_bussi(asap3, equilibrated, dynamicsparams, allraise):
+    propagate(Atoms(equilibrated), asap3,
+              Bussi, dynamicsparams['nvt'])
+
+
+@pytest.mark.optimize()
+@pytest.mark.slow()
+def test_nosehoovernvt(asap3, equilibrated, dynamicsparams, allraise):
+    propagate(Atoms(equilibrated), asap3,
+              NoseHooverChainNVT, dynamicsparams['nosehoover'])
+
+
+@pytest.mark.optimize()
+@pytest.mark.slow()
+def test_nptberendsen(asap3, equilibrated, dynamicsparams, allraise):
+    propagate(Atoms(equilibrated), asap3, NPTBerendsen,
+              dynamicsparams['npt'], max_pressure_error=25.0 * bar)
+
+
+@pytest.mark.optimize()
+@pytest.mark.slow()
+def test_npt_cubic(asap3, equilibrated, dynamicsparams, allraise):
+    propagate(Atoms(equilibrated), asap3, NPT,
+              dynamicsparams['nptold'],
+              max_pressure_error=100 * bar,
+              com_not_thermalized=True)
     # Unlike NPTBerendsen, NPT assumes that the center of mass is not
     # thermalized, so the kinetic energy should be 3/2 ' kB * (N-1) * T
-    n = len(equilibrated)
-    assert abs(t - (n - 1) / n * berendsenparams['npt']['temperature_K']) < 1.0
-    assert abs(p - berendsenparams['npt']['pressure_au']) < 100.0 * bar
+
+
+@pytest.mark.optimize()
+@pytest.mark.slow()
+def test_npt_upper_tri(asap3, equilibrated_upper_tri, dynamicsparams, allraise):
+    # Otherwise, parameters are the same as test_npt
+    propagate(Atoms(equilibrated_upper_tri),
+              asap3,
+              NPT,
+              dynamicsparams['nptold'],
+              max_pressure_error=100 * bar,
+              com_not_thermalized=True)
+
+
+@pytest.mark.optimize()
+@pytest.mark.slow()
+def test_npt_lower_tri(asap3, equilibrated_lower_tri, dynamicsparams, allraise):
+    # Otherwise, parameters are the same as test_npt
+    propagate(Atoms(equilibrated_lower_tri),
+              asap3,
+              NPT,
+              dynamicsparams['nptold'],
+              max_pressure_error=150 * bar,
+              com_not_thermalized=True)
diff -pruN 3.24.0-1/ase/test/md/test_phonon_md_init.py 3.26.0-1/ase/test/md/test_phonon_md_init.py
--- 3.24.0-1/ase/test/md/test_phonon_md_init.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_phonon_md_init.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 from numpy.random import RandomState
diff -pruN 3.24.0-1/ase/test/md/test_rng.py 3.26.0-1/ase/test/md/test_rng.py
--- 3.24.0-1/ase/test/md/test_rng.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_rng.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for legacy and modern NumPy PRNGs."""
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/md/test_velocity_distribution.py 3.26.0-1/ase/test/md/test_velocity_distribution.py
--- 3.24.0-1/ase/test/md/test_velocity_distribution.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_velocity_distribution.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/md/test_verlet_thermostats_asap.py 3.26.0-1/ase/test/md/test_verlet_thermostats_asap.py
--- 3.24.0-1/ase/test/md/test_verlet_thermostats_asap.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/md/test_verlet_thermostats_asap.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/neb/test_COCu111.py 3.26.0-1/ase/test/neb/test_COCu111.py
--- 3.24.0-1/ase/test/neb/test_COCu111.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neb/test_COCu111.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import sqrt
 
 import pytest
diff -pruN 3.24.0-1/ase/test/neb/test_COCu111_2.py 3.26.0-1/ase/test/neb/test_COCu111_2.py
--- 3.24.0-1/ase/test/neb/test_COCu111_2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neb/test_COCu111_2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import sqrt
 
 import pytest
diff -pruN 3.24.0-1/ase/test/neb/test_autoneb.py 3.26.0-1/ase/test/neb/test_autoneb.py
--- 3.24.0-1/ase/test/neb/test_autoneb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neb/test_autoneb.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from pathlib import Path
 
 import pytest
diff -pruN 3.24.0-1/ase/test/neb/test_dynamic_neb.py 3.26.0-1/ase/test/neb/test_dynamic_neb.py
--- 3.24.0-1/ase/test/neb/test_dynamic_neb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neb/test_dynamic_neb.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/neb/test_idpp.py 3.26.0-1/ase/test/neb/test_idpp.py
--- 3.24.0-1/ase/test/neb/test_idpp.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neb/test_idpp.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/neb/test_interpolate_images.py 3.26.0-1/ase/test/neb/test_interpolate_images.py
--- 3.24.0-1/ase/test/neb/test_interpolate_images.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neb/test_interpolate_images.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
@@ -50,7 +51,7 @@ def test_interpolate_images_fixed(images
         image.set_constraint(FixAtoms([0]))
 
     # test raising a RuntimeError here
-    with pytest.raises(RuntimeError, match=r"Constraint\(s\) in image number"):
+    with pytest.raises(RuntimeError, match=r"Constraints in image "):
         interpolate(images)
 
     interpolate(images, apply_constraint=True)
diff -pruN 3.24.0-1/ase/test/neb/test_neb_tr.py 3.26.0-1/ase/test/neb/test_neb_tr.py
--- 3.24.0-1/ase/test/neb/test_neb_tr.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neb/test_neb_tr.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/neb/test_precon_neb.py 3.26.0-1/ase/test/neb/test_precon_neb.py
--- 3.24.0-1/ase/test/neb/test_precon_neb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neb/test_precon_neb.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import json
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/neb/test_shared_calculator_neb.py 3.26.0-1/ase/test/neb/test_shared_calculator_neb.py
--- 3.24.0-1/ase/test/neb/test_shared_calculator_neb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neb/test_shared_calculator_neb.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 This is testing NEB in general, though at the moment focusing on the shared
 calculator implementation that is replacing
diff -pruN 3.24.0-1/ase/test/neighbor/test_neighbor.py 3.26.0-1/ase/test/neighbor/test_neighbor.py
--- 3.24.0-1/ase/test/neighbor/test_neighbor.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neighbor/test_neighbor.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for NeighborList"""
 import numpy as np
 import pytest
@@ -75,7 +76,8 @@ def test_supercell(sorted):
                   cell=[(0.2, 1.2, 1.4),
                         (1.4, 0.1, 1.6),
                         (1.3, 2.0, -0.1)])
-    atoms.set_scaled_positions(3 * np.random.random((10, 3)) - 1)
+    rng = np.random.RandomState(42)
+    atoms.set_scaled_positions(3 * rng.random((10, 3)) - 1)
 
     for p1 in range(2):
         for p2 in range(2):
diff -pruN 3.24.0-1/ase/test/neighbor/test_neighbor_initialization.py 3.26.0-1/ase/test/neighbor/test_neighbor_initialization.py
--- 3.24.0-1/ase/test/neighbor/test_neighbor_initialization.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neighbor/test_neighbor_initialization.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/neighbor/test_neighbor_kernel.py 3.26.0-1/ase/test/neighbor/test_neighbor_kernel.py
--- 3.24.0-1/ase/test/neighbor/test_neighbor_kernel.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/neighbor/test_neighbor_kernel.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
@@ -25,12 +26,12 @@ def test_neighbor_kernel():
     assert (j == np.array([1, 0])).all()
     assert np.abs(d - np.array([np.sqrt(3 / 4), np.sqrt(3 / 4)])).max() < tol
 
+    rng = np.random.RandomState(42)
+
     # test_neighbor_list
     for pbc in [True, False, [True, False, True]]:
         a = ase.Atoms('4001C', cell=[29, 29, 29])
-        a.set_scaled_positions(np.transpose([np.random.random(len(a)),
-                                             np.random.random(len(a)),
-                                             np.random.random(len(a))]))
+        a.set_scaled_positions(rng.random((len(a), 3)))
         j, dr, i, abs_dr, shift = neighbor_list("jDidS", a, 1.85)
 
         assert (np.bincount(i) == np.bincount(j)).all()
@@ -50,9 +51,7 @@ def test_neighbor_kernel():
     # test_neighbor_list_atoms_outside_box
     for pbc in [True, False, [True, False, True]]:
         a = ase.Atoms('4001C', cell=[29, 29, 29])
-        a.set_scaled_positions(np.transpose([np.random.random(len(a)),
-                                             np.random.random(len(a)),
-                                             np.random.random(len(a))]))
+        a.set_scaled_positions(rng.random((len(a), 3)))
         a.set_pbc(pbc)
         a.positions[100, :] += a.cell[0, :]
         a.positions[200, :] += a.cell[1, :]
@@ -170,7 +169,7 @@ def test_neighbor_kernel():
                       cell=[(0.2, 1.2, 1.4),
                             (1.4, 0.1, 1.6),
                             (1.3, 2.0, -0.1)])
-    atoms.set_scaled_positions(3 * np.random.random((nat, 3)) - 1)
+    atoms.set_scaled_positions(3 * rng.random((nat, 3)) - 1)
 
     for p1 in range(2):
         for p2 in range(2):
diff -pruN 3.24.0-1/ase/test/optimize/test_bad_restart.py 3.26.0-1/ase/test/optimize/test_bad_restart.py
--- 3.24.0-1/ase/test/optimize/test_bad_restart.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_bad_restart.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/optimize/test_cellawarebfgs.py 3.26.0-1/ase/test/optimize/test_cellawarebfgs.py
--- 3.24.0-1/ase/test/optimize/test_cellawarebfgs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_cellawarebfgs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
@@ -11,10 +12,9 @@ from ase.units import GPa
 
 
 def test_rattle_supercell_old():
-    """
-       The default len(atoms) to exp_cell_factor acts as a preconditioner
-       and therefore makes the repeat unit cell of rattled atoms to converge
-       in different number of steps.
+    """The default len(atoms) to exp_cell_factor acts as a preconditioner
+    and therefore makes the repeat unit cell of rattled atoms to converge
+    in different number of steps.
     """
     def relax(atoms):
         atoms.calc = EMT()
@@ -40,9 +40,8 @@ def relax(atoms):
 
 
 def test_rattle_supercell():
-    """
-       Make sure that relaxing a rattled cell converges in the same number
-       of iterations than a corresponding supercell with CellAwareBFGS.
+    """Make sure that relaxing a rattled cell converges in the same number
+    of iterations than a corresponding supercell with CellAwareBFGS.
     """
     atoms = bulk('Au')
     atoms *= (2, 1, 1)
@@ -53,12 +52,30 @@ def test_rattle_supercell():
     assert nsteps == nsteps2
 
 
+def test_two_stage_relaxation():
+    """Make sure that we can split relaxation in two stages and relax the
+    structure in the same number of steps.
+    """
+    atoms = bulk('Au')
+    atoms *= (2, 1, 1)
+    atoms.rattle(0.05)
+    # Perform full_relaxation
+    nsteps = relax(atoms.copy())
+    # Perform relaxation in steps
+    atoms.calc = EMT()
+    optimizer = CellAwareBFGS(FrechetCellFilter(atoms, exp_cell_factor=1.0),
+                              alpha=70, long_output=True)
+    optimizer.run(fmax=0.005, smax=0.00005, steps=5)
+    assert optimizer.nsteps == 5
+    optimizer.run(fmax=0.005, smax=0.00005)
+    assert nsteps == optimizer.nsteps
+
+
 @pytest.mark.parametrize('filt', [FrechetCellFilter, UnitCellFilter])
 def test_cellaware_bfgs_2d(filt):
-    """
-       Make sure that the mask works with CellAwareBFGS
-       by requiring that cell vectors on suppressed col and row remain
-       unchanged.
+    """Make sure that the mask works with CellAwareBFGS
+    by requiring that cell vectors on suppressed col and row remain
+    unchanged.
     """
     atoms = fcc110('Au', size=(2, 2, 3), vacuum=4)
     orig_cell = atoms.cell.copy()
@@ -78,9 +95,8 @@ def test_cellaware_bfgs_2d(filt):
 
 
 def test_cellaware_bfgs():
-    """
-       Make sure that a supercell relaxes in same number of steps as the
-       unit cell with CellAwareBFGS.
+    """Make sure that a supercell relaxes in same number of steps as the
+    unit cell with CellAwareBFGS.
     """
     steps = []
     for scale in [1, 2]:
@@ -95,14 +111,13 @@ def test_cellaware_bfgs():
 
 
 def test_elasticity_tensor():
-    """
-       Calculate the exact elasticity tensor. Create an optimizer with
-       that exact hessian, and deform it slightly and verify that within
-       the quadratic reqion, it only takes one step to get back.
-
-       Also verify, that we really set rotation_supression eigenvalues
-       to alpha, and that CellAwareBFGS can approximatily build that exact
-       Hessian within 10% tolerance.
+    """Calculate the exact elasticity tensor. Create an optimizer with
+    that exact hessian, and deform it slightly and verify that within
+    the quadratic reqion, it only takes one step to get back.
+
+    Also verify, that we really set rotation_supression eigenvalues
+    to alpha, and that CellAwareBFGS can approximatily build that exact
+    Hessian within 10% tolerance.
     """
     atoms = bulk('Au')
     atoms *= 2
@@ -111,7 +126,7 @@ def test_elasticity_tensor():
     C_ijkl = get_elasticity_tensor(atoms, verbose=True)
 
     # d = 0.01
-    # deformation = np.eye(3) + d * (np.random.rand(3, 3) - 0.5)
+    # deformation = np.eye(3) + d * (rng.random((3, 3)) - 0.5)
     deformation = np.array([[9.99163386e-01, -8.49034327e-04, -3.1448271e-03],
                             [3.25727960e-03, 9.98723923e-01, 2.76098324e-03],
                             [9.85751768e-04, 4.61517003e-03, 9.95895994e-01]])
diff -pruN 3.24.0-1/ase/test/optimize/test_climb_fix_internals.py 3.26.0-1/ase/test/optimize/test_climb_fix_internals.py
--- 3.24.0-1/ase/test/optimize/test_climb_fix_internals.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_climb_fix_internals.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/optimize/test_fire.py 3.26.0-1/ase/test/optimize/test_fire.py
--- 3.24.0-1/ase/test/optimize/test_fire.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_fire.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/optimize/test_fire2.py 3.26.0-1/ase/test/optimize/test_fire2.py
--- 3.24.0-1/ase/test/optimize/test_fire2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_fire2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/optimize/test_maxstep.py 3.26.0-1/ase/test/optimize/test_maxstep.py
--- 3.24.0-1/ase/test/optimize/test_maxstep.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_maxstep.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/optimize/test_minimahop.py 3.26.0-1/ase/test/optimize/test_minimahop.py
--- 3.24.0-1/ase/test/optimize/test_minimahop.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_minimahop.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atom, Atoms
diff -pruN 3.24.0-1/ase/test/optimize/test_nsteps.py 3.26.0-1/ase/test/optimize/test_nsteps.py
--- 3.24.0-1/ase/test/optimize/test_nsteps.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_nsteps.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/optimize/test_optimize_stepwise.py 3.26.0-1/ase/test/optimize/test_optimize_stepwise.py
--- 3.24.0-1/ase/test/optimize/test_optimize_stepwise.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_optimize_stepwise.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/optimize/test_optimizer_restart.py 3.26.0-1/ase/test/optimize/test_optimizer_restart.py
--- 3.24.0-1/ase/test/optimize/test_optimizer_restart.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_optimizer_restart.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import shutil
 from math import sqrt
 from os.path import getsize
diff -pruN 3.24.0-1/ase/test/optimize/test_optimizers.py 3.26.0-1/ase/test/optimize/test_optimizers.py
--- 3.24.0-1/ase/test/optimize/test_optimizers.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_optimizers.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,6 @@
+# fmt: off
+from pathlib import Path
+
 import pytest
 
 from ase.build import bulk
@@ -97,7 +100,8 @@ def test_unconverged(optcls, atoms, kwar
     fmax = 1e-9  # small value to not get converged
     with optcls(atoms, **kwargs) as opt:
         opt.run(fmax=fmax, steps=1)  # only one step to not get converged
-    assert not opt.converged()
+    gradient = opt.optimizable.get_gradient()
+    assert not opt.converged(gradient)
     assert opt.todict()["fmax"] == 1e-9
 
 
@@ -110,3 +114,13 @@ def test_run_twice(optcls, atoms, kwargs
         opt.run(fmax=fmax, steps=steps)
     assert opt.nsteps == 2 * steps
     assert opt.max_steps == 2 * steps
+
+
+@pytest.mark.optimize()
+@pytest.mark.filterwarnings("ignore: estimate_mu")
+def test_path(testdir, optcls, atoms, kwargs):
+    fmax = 0.01
+    traj, log = Path('trajectory.traj'), Path('relax.log')
+    with optcls(atoms, logfile=log, trajectory=traj, **kwargs) as opt:
+        is_converged = opt.run(fmax=fmax)
+    assert is_converged  # check if opt.run() returns True when converged
diff -pruN 3.24.0-1/ase/test/optimize/test_pure.py 3.26.0-1/ase/test/optimize/test_pure.py
--- 3.24.0-1/ase/test/optimize/test_pure.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_pure.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,57 @@
+import numpy as np
+import pytest
+
+from ase.utils.abc import Optimizable
+
+
+class BoothFunctionOptimizable(Optimizable):
+    """Optimizable based on the “Booth” function.
+
+    https://en.wikipedia.org/wiki/Test_functions_for_optimization
+    """
+
+    def __init__(self, x0):
+        self.xy = np.array(x0)
+
+    def get_x(self):
+        return self.xy.copy()
+
+    def set_x(self, x):
+        self.xy[:] = x
+
+    @staticmethod
+    def ab(x, y):
+        return x + 2 * y - 7, 2 * x + y - 5
+
+    def get_value(self):
+        a, b = self.ab(*self.xy)
+        return a * a + b * b
+
+    def get_gradient(self):
+        x, y = self.xy
+        a, b = self.ab(*self.xy)
+        # XXX negative gradient
+        return -np.array([2 * a + 4 * b, 4 * a + 2 * b])
+
+    def iterimages(self):
+        return iter([])
+
+    def ndofs(self):
+        return len(self.xy)
+
+    def gradient_norm(self, gradient):
+        return np.linalg.norm(gradient)
+
+
+def test_booth():
+    from ase.optimize.bfgs import BFGS
+
+    x0 = [1.234, 2.345]
+    target = BoothFunctionOptimizable(x0)
+
+    eps = 1e-8
+    with BFGS(target) as opt:
+        opt.run(fmax=eps)
+
+    assert target.xy == pytest.approx([1, 3], abs=eps)
+    assert target.get_value() == pytest.approx(0, abs=eps**2)
diff -pruN 3.24.0-1/ase/test/optimize/test_replay.py 3.26.0-1/ase/test/optimize/test_replay.py
--- 3.24.0-1/ase/test/optimize/test_replay.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/optimize/test_replay.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from math import sqrt
 
 import pytest
diff -pruN 3.24.0-1/ase/test/precon/test_amin.py 3.26.0-1/ase/test/precon/test_amin.py
--- 3.24.0-1/ase/test/precon/test_amin.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/precon/test_amin.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/precon/test_ff_and_precon_c60.py 3.26.0-1/ase/test/precon/test_ff_and_precon_c60.py
--- 3.24.0-1/ase/test/precon/test_ff_and_precon_c60.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/precon/test_ff_and_precon_c60.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/precon/test_lbfgs.py 3.26.0-1/ase/test/precon/test_lbfgs.py
--- 3.24.0-1/ase/test/precon/test_lbfgs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/precon/test_lbfgs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/precon/test_precon_assembly.py 3.26.0-1/ase/test/precon/test_precon_assembly.py
--- 3.24.0-1/ase/test/precon/test_precon_assembly.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/precon/test_precon_assembly.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/precon/test_smallcell.py 3.26.0-1/ase/test/precon/test_smallcell.py
--- 3.24.0-1/ase/test/precon/test_smallcell.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/precon/test_smallcell.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.atoms import Atoms
diff -pruN 3.24.0-1/ase/test/precon/test_unitcellfilter.py 3.26.0-1/ase/test/precon/test_unitcellfilter.py
--- 3.24.0-1/ase/test/precon/test_unitcellfilter.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/precon/test_unitcellfilter.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 import ase
diff -pruN 3.24.0-1/ase/test/spacegroup/test_minerals.py 3.26.0-1/ase/test/spacegroup/test_minerals.py
--- 3.24.0-1/ase/test/spacegroup/test_minerals.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/spacegroup/test_minerals.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from dataclasses import dataclass
 from itertools import product
 from pathlib import Path
diff -pruN 3.24.0-1/ase/test/spacegroup/test_space_group_data.py 3.26.0-1/ase/test/spacegroup/test_space_group_data.py
--- 3.24.0-1/ase/test/spacegroup/test_space_group_data.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/spacegroup/test_space_group_data.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 from numpy.testing import assert_allclose
diff -pruN 3.24.0-1/ase/test/spacegroup/test_spacegroup.py 3.26.0-1/ase/test/spacegroup/test_spacegroup.py
--- 3.24.0-1/ase/test/spacegroup/test_spacegroup.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/spacegroup/test_spacegroup.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/spacegroup/test_spacegroup_crystal.py 3.26.0-1/ase/test/spacegroup/test_spacegroup_crystal.py
--- 3.24.0-1/ase/test/spacegroup/test_spacegroup_crystal.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/spacegroup/test_spacegroup_crystal.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.io import write
diff -pruN 3.24.0-1/ase/test/spacegroup/test_spacegroup_utils.py 3.26.0-1/ase/test/spacegroup/test_spacegroup_utils.py
--- 3.24.0-1/ase/test/spacegroup/test_spacegroup_utils.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/spacegroup/test_spacegroup_utils.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/spectrum/test_doscollection.py 3.26.0-1/ase/test/spectrum/test_doscollection.py
--- 3.24.0-1/ase/test/spectrum/test_doscollection.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/spectrum/test_doscollection.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from typing import Iterable
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/spectrum/test_dosdata.py 3.26.0-1/ase/test/spectrum/test_dosdata.py
--- 3.24.0-1/ase/test/spectrum/test_dosdata.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/spectrum/test_dosdata.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from collections import OrderedDict
 from typing import Any, List, Tuple
 
diff -pruN 3.24.0-1/ase/test/standardization/test_properties.py 3.26.0-1/ase/test/standardization/test_properties.py
--- 3.24.0-1/ase/test/standardization/test_properties.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/standardization/test_properties.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk
 from ase.calculators.emt import EMT
 
diff -pruN 3.24.0-1/ase/test/test_bader.py 3.26.0-1/ase/test/test_bader.py
--- 3.24.0-1/ase/test/test_bader.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_bader.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from pathlib import Path
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/test_bandpath_kpts_axis.py 3.26.0-1/ase/test/test_bandpath_kpts_axis.py
--- 3.24.0-1/ase/test/test_bandpath_kpts_axis.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_bandpath_kpts_axis.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 
 
diff -pruN 3.24.0-1/ase/test/test_basin.py 3.26.0-1/ase/test/test_basin.py
--- 3.24.0-1/ase/test/test_basin.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_basin.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
@@ -20,8 +21,8 @@ def test_basin(testdir):
         7: -16.505384}
     N = 7
     R = N**(1. / 3.)
-    np.random.seed(42)
-    pos = np.random.uniform(-R, R, (N, 3))
+    rng = np.random.RandomState(42)
+    pos = rng.uniform(-R, R, (N, 3))
     s = Atoms('He' + str(N),
               positions=pos)
     s.calc = LennardJones()
diff -pruN 3.24.0-1/ase/test/test_calc_outputs_big.py 3.26.0-1/ase/test/test_calc_outputs_big.py
--- 3.24.0-1/ase/test/test_calc_outputs_big.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_calc_outputs_big.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_calc_properties.py 3.26.0-1/ase/test/test_calc_properties.py
--- 3.24.0-1/ase/test/test_calc_properties.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_calc_properties.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_calculator_equal.py 3.26.0-1/ase/test/test_calculator_equal.py
--- 3.24.0-1/ase/test/test_calculator_equal.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_calculator_equal.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_calculator_label.py 3.26.0-1/ase/test/test_calculator_label.py
--- 3.24.0-1/ase/test/test_calculator_label.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_calculator_label.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.calculators.calculator import Calculator
 
 
diff -pruN 3.24.0-1/ase/test/test_checkpoint.py 3.26.0-1/ase/test/test_checkpoint.py
--- 3.24.0-1/ase/test/test_checkpoint.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_checkpoint.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atom
@@ -39,25 +40,25 @@ def rattle_calc(atoms, calc):
     orig_atoms = atoms.copy()
 
     # first do a couple of calculations
-    np.random.seed(0)
-    atoms.rattle()
+    rng = np.random.RandomState(0)
+    atoms.rattle(rng=rng)
     cp_calc_1 = CheckpointCalculator(calc)
     atoms.calc = cp_calc_1
     e11 = atoms.get_potential_energy()
     f11 = atoms.get_forces()
-    atoms.rattle()
+    atoms.rattle(rng=rng)
     e12 = atoms.get_potential_energy()
     f12 = atoms.get_forces()
 
     # then re-read them from checkpoint file
     atoms = orig_atoms
-    np.random.seed(0)
-    atoms.rattle()
+    rng = np.random.RandomState(0)
+    atoms.rattle(rng=rng)
     cp_calc_2 = CheckpointCalculator(calc)
     atoms.calc = cp_calc_2
     e21 = atoms.get_potential_energy()
     f21 = atoms.get_forces()
-    atoms.rattle()
+    atoms.rattle(rng=rng)
     e22 = atoms.get_potential_energy()
     f22 = atoms.get_forces()
 
diff -pruN 3.24.0-1/ase/test/test_cluster.py 3.26.0-1/ase/test/test_cluster.py
--- 3.24.0-1/ase/test/test_cluster.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_cluster.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_cutoffs.py 3.26.0-1/ase/test/test_cutoffs.py
--- 3.24.0-1/ase/test/test_cutoffs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_cutoffs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/test_dependency_list.py 3.26.0-1/ase/test/test_dependency_list.py
--- 3.24.0-1/ase/test/test_dependency_list.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_dependency_list.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.dependencies import format_dependency
 
 
diff -pruN 3.24.0-1/ase/test/test_diffusion_coefficient.py 3.26.0-1/ase/test/test_diffusion_coefficient.py
--- 3.24.0-1/ase/test/test_diffusion_coefficient.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_diffusion_coefficient.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.atoms import Atoms
 from ase.md.analysis import DiffusionCoefficient
 from ase.units import fs as fs_conversion
diff -pruN 3.24.0-1/ase/test/test_dimer.py 3.26.0-1/ase/test/test_dimer.py
--- 3.24.0-1/ase/test/test_dimer.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_dimer.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atom, Atoms
 from ase.calculators.lj import LennardJones
 from ase.constraints import FixBondLength
diff -pruN 3.24.0-1/ase/test/test_dimer_method.py 3.26.0-1/ase/test/test_dimer_method.py
--- 3.24.0-1/ase/test/test_dimer_method.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_dimer_method.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import add_adsorbate, fcc100
diff -pruN 3.24.0-1/ase/test/test_distmom.py 3.26.0-1/ase/test/test_distmom.py
--- 3.24.0-1/ase/test/test_distmom.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_distmom.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.dft import get_distribution_moment
diff -pruN 3.24.0-1/ase/test/test_doctests.py 3.26.0-1/ase/test/test_doctests.py
--- 3.24.0-1/ase/test/test_doctests.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_doctests.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import doctest
 import importlib
 
@@ -17,7 +18,7 @@ ase.geometry.cell
 ase.geometry.geometry
 ase.io.ulm
 ase.lattice
-ase.phasediagram
+ase.neighborlist
 ase.spacegroup.spacegroup
 ase.spacegroup.xtal
 ase.symbols
@@ -25,12 +26,7 @@ ase.symbols
 
 
 @pytest.mark.parametrize('modname', module_names)
-def test_doctest(testdir, modname, recwarn):
+def test_doctest(testdir, modname):
     mod = importlib.import_module(modname)
     with np.printoptions(legacy='1.13'):
         doctest.testmod(mod, raise_on_error=True, verbose=True)
-        nwarnings = len(recwarn.list)
-        if modname == 'ase.phasediagram':
-            assert nwarnings == 1
-        else:
-            assert nwarnings == 0
diff -pruN 3.24.0-1/ase/test/test_dos.py 3.26.0-1/ase/test/test_dos.py
--- 3.24.0-1/ase/test/test_dos.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_dos.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.dft.dos import linear_tetrahedron_integration as lti
diff -pruN 3.24.0-1/ase/test/test_eos.py 3.26.0-1/ase/test/test_eos.py
--- 3.24.0-1/ase/test/test_eos.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_eos.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/test_example.py 3.26.0-1/ase/test/test_example.py
--- 3.24.0-1/ase/test/test_example.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_example.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/test_external_viewer.py 3.26.0-1/ase/test/test_external_viewer.py
--- 3.24.0-1/ase/test/test_external_viewer.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_external_viewer.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import sys
 
 import pytest
diff -pruN 3.24.0-1/ase/test/test_filecache.py 3.26.0-1/ase/test/test_filecache.py
--- 3.24.0-1/ase/test/test_filecache.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_filecache.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_forcecurve.py 3.26.0-1/ase/test/test_forcecurve.py
--- 3.24.0-1/ase/test/test_forcecurve.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_forcecurve.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk
 from ase.calculators.emt import EMT
 from ase.io import read
diff -pruN 3.24.0-1/ase/test/test_formula.py 3.26.0-1/ase/test/test_formula.py
--- 3.24.0-1/ase/test/test_formula.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_formula.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_grid_slice.py 3.26.0-1/ase/test/test_grid_slice.py
--- 3.24.0-1/ase/test/test_grid_slice.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_grid_slice.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.utils.cube import grid_2d_slice
diff -pruN 3.24.0-1/ase/test/test_gromacs.py 3.26.0-1/ase/test/test_gromacs.py
--- 3.24.0-1/ase/test/test_gromacs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_gromacs.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 from io import StringIO
 
diff -pruN 3.24.0-1/ase/test/test_hcp.py 3.26.0-1/ase/test/test_hcp.py
--- 3.24.0-1/ase/test/test_hcp.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_hcp.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 from scipy.optimize import fmin_bfgs
 
diff -pruN 3.24.0-1/ase/test/test_imports.py 3.26.0-1/ase/test/test_imports.py
--- 3.24.0-1/ase/test/test_imports.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_imports.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import contextlib
 from importlib import import_module
 from pathlib import Path
diff -pruN 3.24.0-1/ase/test/test_isotopes.py 3.26.0-1/ase/test/test_isotopes.py
--- 3.24.0-1/ase/test/test_isotopes.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_isotopes.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.data.isotopes import parse_isotope_data
 
 
diff -pruN 3.24.0-1/ase/test/test_issue276.py 3.26.0-1/ase/test/test_issue276.py
--- 3.24.0-1/ase/test/test_issue276.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_issue276.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/test_kpts.py 3.26.0-1/ase/test/test_kpts.py
--- 3.24.0-1/ase/test/test_kpts.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_kpts.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.dft.kpoints import bandpath
diff -pruN 3.24.0-1/ase/test/test_kpts2kpts.py 3.26.0-1/ase/test/test_kpts2kpts.py
--- 3.24.0-1/ase/test/test_kpts2kpts.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_kpts2kpts.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/test_lattice_lindep.py 3.26.0-1/ase/test/test_lattice_lindep.py
--- 3.24.0-1/ase/test/test_lattice_lindep.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_lattice_lindep.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.lattice.cubic import FaceCenteredCubic
diff -pruN 3.24.0-1/ase/test/test_linesearch_maxstep.py 3.26.0-1/ase/test/test_linesearch_maxstep.py
--- 3.24.0-1/ase/test/test_linesearch_maxstep.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_linesearch_maxstep.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_lock.py 3.26.0-1/ase/test/test_lock.py
--- 3.24.0-1/ase/test/test_lock.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_lock.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.utils import Lock
diff -pruN 3.24.0-1/ase/test/test_loggingcalc.py 3.26.0-1/ase/test/test_loggingcalc.py
--- 3.24.0-1/ase/test/test_loggingcalc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_loggingcalc.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk
 from ase.calculators.emt import EMT
 from ase.calculators.loggingcalc import LoggingCalculator
diff -pruN 3.24.0-1/ase/test/test_makebandpath.py 3.26.0-1/ase/test/test_makebandpath.py
--- 3.24.0-1/ase/test/test_makebandpath.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_makebandpath.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk
 from ase.dft.kpoints import bandpath
 
diff -pruN 3.24.0-1/ase/test/test_matplotlib_plot.py 3.26.0-1/ase/test/test_matplotlib_plot.py
--- 3.24.0-1/ase/test/test_matplotlib_plot.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_matplotlib_plot.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/test_minimum_image_convention.py 3.26.0-1/ase/test/test_minimum_image_convention.py
--- 3.24.0-1/ase/test/test_minimum_image_convention.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_minimum_image_convention.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 from numpy.testing import assert_allclose
diff -pruN 3.24.0-1/ase/test/test_mixingcalc.py 3.26.0-1/ase/test/test_mixingcalc.py
--- 3.24.0-1/ase/test/test_mixingcalc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_mixingcalc.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_mpi.py 3.26.0-1/ase/test/test_mpi.py
--- 3.24.0-1/ase/test/test_mpi.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_mpi.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import sys
 from subprocess import run
 
diff -pruN 3.24.0-1/ase/test/test_noncollinear.py 3.26.0-1/ase/test/test_noncollinear.py
--- 3.24.0-1/ase/test/test_noncollinear.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_noncollinear.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase import Atoms
 
 
diff -pruN 3.24.0-1/ase/test/test_nwchem_writer.py 3.26.0-1/ase/test/test_nwchem_writer.py
--- 3.24.0-1/ase/test/test_nwchem_writer.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_nwchem_writer.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_parsemath.py 3.26.0-1/ase/test/test_parsemath.py
--- 3.24.0-1/ase/test/test_parsemath.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_parsemath.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import math
 
 from ase.utils.parsemath import eval_expression
diff -pruN 3.24.0-1/ase/test/test_pathlib_support.py 3.26.0-1/ase/test/test_pathlib_support.py
--- 3.24.0-1/ase/test/test_pathlib_support.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_pathlib_support.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test reading/writing in ASE on pathlib objects"""
 
 import io
diff -pruN 3.24.0-1/ase/test/test_phasediagram.py 3.26.0-1/ase/test/test_phasediagram.py
--- 3.24.0-1/ase/test/test_phasediagram.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_phasediagram.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test phasediagram code."""
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_phonons.py 3.26.0-1/ase/test/test_phonons.py
--- 3.24.0-1/ase/test/test_phonons.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_phonons.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import os
 
 import numpy as np
diff -pruN 3.24.0-1/ase/test/test_potential_energies.py 3.26.0-1/ase/test/test_potential_energies.py
--- 3.24.0-1/ase/test/test_potential_energies.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_potential_energies.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from numpy.testing import assert_allclose
 
 import ase.build
diff -pruN 3.24.0-1/ase/test/test_pourbaix.py 3.26.0-1/ase/test/test_pourbaix.py
--- 3.24.0-1/ase/test/test_pourbaix.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_pourbaix.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test Pourbaix diagram."""
 import numpy as np
 import pytest
diff -pruN 3.24.0-1/ase/test/test_properties.py 3.26.0-1/ase/test/test_properties.py
--- 3.24.0-1/ase/test/test_properties.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_properties.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/test_ptable.py 3.26.0-1/ase/test/test_ptable.py
--- 3.24.0-1/ase/test/test_ptable.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_ptable.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,5 @@
 #!/usr/bin/env python3
+# fmt: off
 """Test Periodic Table."""
 from ase.utils.ptable import ptable
 
diff -pruN 3.24.0-1/ase/test/test_pubchem.py 3.26.0-1/ase/test/test_pubchem.py
--- 3.24.0-1/ase/test/test_pubchem.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_pubchem.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,14 @@
+# fmt: off
+"""Tests for PubChem."""
+
+import json
+from io import BytesIO
+
+import pytest
+
+from ase.data import pubchem
 from ase.data.pubchem import (
+    analyze_input,
     pubchem_atoms_conformer_search,
     pubchem_atoms_search,
     pubchem_conformer_search,
@@ -6,32 +16,67 @@ from ase.data.pubchem import (
 )
 
 
-def test_pubchem():
+@pytest.fixture
+def mock_pubchem(monkeypatch) -> None:
+    """Mock `pubchem`."""
+
+    def mock_search_pubchem_raw_222(*args, **kwargs):
+        """Mock `search_pubchem_raw` for ammonia (CID=222)."""
+        data222 = b'222\n  -OEChem-10071914343D\n\n  4  3  0     0  0  0  0  0  0999 V2000\n    0.0000    0.0000    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0\n   -0.4417    0.2906    0.8711 H   0  0  0  0  0  0  0  0  0  0  0  0\n    0.7256    0.6896   -0.1907 H   0  0  0  0  0  0  0  0  0  0  0  0\n    0.4875   -0.8701    0.2089 H   0  0  0  0  0  0  0  0  0  0  0  0\n  1  2  1  0  0  0  0\n  1  3  1  0  0  0  0\n  1  4  1  0  0  0  0\nM  END\n> <PUBCHEM_COMPOUND_CID>\n222\n\n> <PUBCHEM_CONFORMER_RMSD>\n0.4\n\n> <PUBCHEM_CONFORMER_DIVERSEORDER>\n1\n\n> <PUBCHEM_MMFF94_PARTIAL_CHARGES>\n4\n1 -1.08\n2 0.36\n3 0.36\n4 0.36\n\n> <PUBCHEM_EFFECTIVE_ROTOR_COUNT>\n0\n\n> <PUBCHEM_PHARMACOPHORE_FEATURES>\n1\n1 1 cation\n\n> <PUBCHEM_HEAVY_ATOM_COUNT>\n1\n\n> <PUBCHEM_ATOM_DEF_STEREO_COUNT>\n0\n\n> <PUBCHEM_ATOM_UDEF_STEREO_COUNT>\n0\n\n> <PUBCHEM_BOND_DEF_STEREO_COUNT>\n0\n\n> <PUBCHEM_BOND_UDEF_STEREO_COUNT>\n0\n\n> <PUBCHEM_ISOTOPIC_ATOM_COUNT>\n0\n\n> <PUBCHEM_COMPONENT_COUNT>\n1\n\n> <PUBCHEM_CACTVS_TAUTO_COUNT>\n1\n\n> <PUBCHEM_CONFORMER_ID>\n000000DE00000001\n\n> <PUBCHEM_MMFF94_ENERGY>\n0\n\n> <PUBCHEM_FEATURE_SELFOVERLAP>\n5.074\n\n> <PUBCHEM_SHAPE_FINGERPRINT>\n260 1 18410856563934756871\n\n> <PUBCHEM_SHAPE_MULTIPOLES>\n15.6\n0.51\n0.51\n0.51\n0\n0\n0\n0\n0\n0\n0\n0\n0\n0\n\n> <PUBCHEM_SHAPE_SELFOVERLAP>\n14.89\n\n> <PUBCHEM_SHAPE_VOLUME>\n15.6\n\n> <PUBCHEM_COORDINATE_TYPE>\n2\n5\n10\n\n$$$$\n'  # noqa
+        r = BytesIO(data222)
+        return r.read().decode('utf-8')
+
+    def mock_available_conformer_search_222(*args, **kwargs):
+        """Mock `available_conformer_search` for ammonia (CID=222)."""
+        conformer222 = b'{\n  "InformationList": {\n    "Information": [\n      {\n        "CID": 222,\n        "ConformerID": [\n          "000000DE00000001"\n        ]\n      }\n    ]\n  }\n}\n'  # noqa
+        r = BytesIO(conformer222)
+        record = r.read().decode('utf-8')
+        record = json.loads(record)
+        return record['InformationList']['Information'][0]['ConformerID']
+
+    f = mock_search_pubchem_raw_222
+    monkeypatch.setattr(pubchem, 'search_pubchem_raw', f)
+
+    f = mock_available_conformer_search_222
+    monkeypatch.setattr(pubchem, 'available_conformer_search', f)
+
+
+def test_pubchem_search(mock_pubchem) -> None:
+    """Test if `pubchem_search` handles given arguments correctly."""
+    data = pubchem_search('ammonia')
+
+    atoms = data.get_atoms()
+    assert atoms.get_chemical_symbols() == ['N', 'H', 'H', 'H']
 
-    # check class functionality
-    data = pubchem_search('ammonia', mock_test=True)
-    data.get_atoms()
     data.get_pubchem_data()
-    # XXX maybe verify some of this data?
 
+
+def test_pubchem(mock_pubchem) -> None:
     # check the various entry styles and the functions that return atoms
-    pubchem_search(cid=241, mock_test=True).get_atoms()
-    pubchem_atoms_search(smiles='CCOH', mock_test=True)
-    pubchem_atoms_conformer_search('octane', mock_test=True)
-    # (maybe test something about some of the returned atoms)
+    pubchem_search(cid=241).get_atoms()
+    pubchem_atoms_search(smiles='CCOH')
+    pubchem_atoms_conformer_search('octane')
+
 
-    # check conformer searching
-    confs = pubchem_conformer_search('octane', mock_test=True)
+def test_pubchem_conformer_search(mock_pubchem) -> None:
+    """Test if `pubchem_conformer_search` runs as expected."""
+    confs = pubchem_conformer_search('octane')
     for _ in confs:
         pass
-    try:  # check that you can't pass in two args
-        pubchem_search(name='octane', cid=222, mock_test=True)
-        raise Exception('Test Failed')
-    except ValueError:
-        pass
 
-    try:  # check that you must pass at least one arg
-        pubchem_search(mock_test=True)
-        raise Exception('Test Failed')
-    except ValueError:
-        pass
+
+def test_multiple_search(mock_pubchem) -> None:
+    """Check that you can't pass in two args."""
+    with pytest.raises(ValueError):
+        pubchem_search(name='octane', cid=222)
+
+
+def test_empty_search(mock_pubchem) -> None:
+    """Check that you must pass at least one arg."""
+    with pytest.raises(ValueError):
+        pubchem_search()
+
+
+def test_triple_bond() -> None:
+    """Check if hash (`#`) is converted to hex (`%23`)."""
+    assert analyze_input(smiles='CC#N')[0] == 'CC%23N'
diff -pruN 3.24.0-1/ase/test/test_quaternions.py 3.26.0-1/ase/test/test_quaternions.py
--- 3.24.0-1/ase/test/test_quaternions.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_quaternions.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_root_surf.py 3.26.0-1/ase/test/test_root_surf.py
--- 3.24.0-1/ase/test/test_root_surf.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_root_surf.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import (
diff -pruN 3.24.0-1/ase/test/test_s22.py 3.26.0-1/ase/test/test_s22.py
--- 3.24.0-1/ase/test/test_s22.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_s22.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.collections import s22
 
 
diff -pruN 3.24.0-1/ase/test/test_singlepoint_dft_calc.py 3.26.0-1/ase/test/test_singlepoint_dft_calc.py
--- 3.24.0-1/ase/test/test_singlepoint_dft_calc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_singlepoint_dft_calc.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/test_singlepointcalc.py 3.26.0-1/ase/test/test_singlepointcalc.py
--- 3.24.0-1/ase/test/test_singlepointcalc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_singlepointcalc.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import fcc111
 from ase.calculators.emt import EMT
 from ase.constraints import FixAtoms
diff -pruN 3.24.0-1/ase/test/test_springcalc.py 3.26.0-1/ase/test/test_springcalc.py
--- 3.24.0-1/ase/test/test_springcalc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_springcalc.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/test_stm.py 3.26.0-1/ase/test/test_stm.py
--- 3.24.0-1/ase/test/test_stm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_stm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.calculators.test import make_test_dft_calculation
 from ase.dft.stm import STM
 
diff -pruN 3.24.0-1/ase/test/test_stress.py 3.26.0-1/ase/test/test_stress.py
--- 3.24.0-1/ase/test/test_stress.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_stress.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_structure_comparator.py 3.26.0-1/ase/test/test_structure_comparator.py
--- 3.24.0-1/ase/test/test_structure_comparator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_structure_comparator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from random import randint
 
 import numpy as np
@@ -100,10 +101,12 @@ def test_rotations_to_standard(comparato
     s1 = Atoms("Al")
     tol = 1E-6
     num_tests = 4
+    rng = np.random.RandomState(42)
+
     if heavy_test:
         num_tests = 20
     for _ in range(num_tests):
-        cell = np.random.rand(3, 3) * 4.0 - 4.0
+        cell = rng.random((3, 3)) * 4.0 - 4.0
         s1.set_cell(cell)
         new_cell = comparator._standarize_cell(s1).get_cell().T
         assert abs(new_cell[1, 0]) < tol
diff -pruN 3.24.0-1/ase/test/test_symbols.py 3.26.0-1/ase/test/test_symbols.py
--- 3.24.0-1/ase/test/test_symbols.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_symbols.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/test_thermochemistry.py 3.26.0-1/ase/test/test_thermochemistry.py
--- 3.24.0-1/ase/test/test_thermochemistry.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_thermochemistry.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
diff -pruN 3.24.0-1/ase/test/test_things.py 3.26.0-1/ase/test/test_things.py
--- 3.24.0-1/ase/test/test_things.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_things.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import bulk
 from ase.dft.kpoints import monkhorst_pack
 from ase.units import Bohr, Hartree, fs, kB, kcal, kJ, mol
diff -pruN 3.24.0-1/ase/test/test_timing.py 3.26.0-1/ase/test/test_timing.py
--- 3.24.0-1/ase/test/test_timing.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_timing.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import time
 
 from ase.utils.timing import Timer, timer
diff -pruN 3.24.0-1/ase/test/test_units.py 3.26.0-1/ase/test/test_units.py
--- 3.24.0-1/ase/test/test_units.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_units.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """This test cross-checks our implementation of CODATA against the
 implementation that SciPy brings with it.
 """
diff -pruN 3.24.0-1/ase/test/test_util.py 3.26.0-1/ase/test/test_util.py
--- 3.24.0-1/ase/test/test_util.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_util.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from typing import Any, Dict, List
 
 import pytest
diff -pruN 3.24.0-1/ase/test/test_versionnumber.py 3.26.0-1/ase/test/test_versionnumber.py
--- 3.24.0-1/ase/test/test_versionnumber.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_versionnumber.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from importlib.metadata import PackageNotFoundError, version
 
 import pytest
diff -pruN 3.24.0-1/ase/test/test_x3d.py 3.26.0-1/ase/test/test_x3d.py
--- 3.24.0-1/ase/test/test_x3d.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_x3d.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import bulk
diff -pruN 3.24.0-1/ase/test/test_xrdebye.py 3.26.0-1/ase/test/test_xrdebye.py
--- 3.24.0-1/ase/test/test_xrdebye.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_xrdebye.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Tests for XrDebye class"""
 
 from pathlib import Path
diff -pruN 3.24.0-1/ase/test/test_xwopen.py 3.26.0-1/ase/test/test_xwopen.py
--- 3.24.0-1/ase/test/test_xwopen.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_xwopen.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.utils import xwopen
diff -pruN 3.24.0-1/ase/test/test_zmatrix.py 3.26.0-1/ase/test/test_zmatrix.py
--- 3.24.0-1/ase/test/test_zmatrix.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/test_zmatrix.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import numpy as np
 import pytest
 
Binary files 3.24.0-1/ase/test/testdata/rotated_bz.png and 3.26.0-1/ase/test/testdata/rotated_bz.png differ
diff -pruN 3.24.0-1/ase/test/testdata/tersoff/SiC.tersoff 3.26.0-1/ase/test/testdata/tersoff/SiC.tersoff
--- 3.24.0-1/ase/test/testdata/tersoff/SiC.tersoff	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/ase/test/testdata/tersoff/SiC.tersoff	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,41 @@
+# DATE: 2011-04-26 UNITS: metal CONTRIBUTOR: Aidan Thompson, athomps@sandia.gov CITATION: Tersoff, Phys Rev B, 39, 5566-5568 (1989)
+
+# Si and C mixture, parameterized for Tersoff potential
+# this file is from Rutuparna.Narulkar @ okstate.edu
+# values are from Phys Rev B, 39, 5566-5568 (1989)
+# and errata (PRB 41, 3248)
+
+# Tersoff parameters for various elements and mixtures
+# multiple entries can be added to this file, LAMMPS reads the ones it needs
+# these entries are in LAMMPS "metal" units:
+#   A,B = eV; lambda1,lambda2,lambda3 = 1/Angstroms; R,D = Angstroms
+#   other quantities are unitless
+
+# format of a single entry (one or more lines):
+#   element 1, element 2, element 3,
+#               m, gamma, lambda3, c, d, costheta0, n,
+#               beta, lambda2, B, R, D, lambda1, A
+
+C   C    C   3.0 1.0 0.0 38049  4.3484   -.57058 .72751
+             0.00000015724 2.2119  346.7   1.95   0.15   3.4879  1393.6
+
+Si  Si  Si  3.0 1.0 0.0  100390  16.217   -.59825 .78734
+            0.0000011     1.73222  471.18  2.85   0.15    2.4799  1830.8
+
+Si  Si  C   3.0 1.0 0.0 100390  16.217   -.59825 0.0
+            0.0 0.0 0.0 2.36   0.15 0.0 0.0
+
+Si  C   C   3.0 1.0 0.0 100390 16.217 -.59825 .787340
+            0.0000011     1.97205 395.126  2.36  0.15    2.9839  1597.3111
+
+C   Si  Si  3.0 1.0 0.0 38049  4.3484  -.57058 .72751
+            0.00000015724 1.97205 395.126  2.36  0.15   2.9839   1597.3111
+
+C   Si  C   3.0 1.0 0.0 38049  4.3484   -.57058 0.0
+            0.0 0.0 0.0 1.95   0.15 0.0 0.0
+
+C   C   Si  3.0 1.0 0.0 38049  4.3484   -.57058 0.0
+            0.0 0.0 0.0 2.36   0.15 0.0 0.0
+
+Si  C   Si  3.0 1.0 0.0 100390 16.217 -.59825 0.0
+            0.0 0.0 0.0 2.85   0.15 0.0 0.0
diff -pruN 3.24.0-1/ase/test/testsuite.py 3.26.0-1/ase/test/testsuite.py
--- 3.24.0-1/ase/test/testsuite.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/testsuite.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import argparse
 import os
 import sys
@@ -55,7 +56,7 @@ def have_module(module):
     return importlib.util.find_spec(module) is not None
 
 
-MULTIPROCESSING_MAX_WORKERS = 32
+MULTIPROCESSING_MAX_AUTO_WORKERS = 8
 MULTIPROCESSING_DISABLED = 0
 MULTIPROCESSING_AUTO = -1
 
@@ -64,7 +65,7 @@ def choose_how_many_workers(jobs):
 
     if jobs == MULTIPROCESSING_AUTO:
         if have_module('xdist'):
-            jobs = min(cpu_count(), MULTIPROCESSING_MAX_WORKERS)
+            jobs = min(cpu_count(), MULTIPROCESSING_MAX_AUTO_WORKERS)
         else:
             jobs = MULTIPROCESSING_DISABLED
     return jobs
@@ -128,7 +129,7 @@ class CLICommand:
             help='number of worker processes.  If pytest-xdist is available,'
             ' defaults to all available processors up to a maximum of {}.  '
             '0 disables multiprocessing'
-            .format(MULTIPROCESSING_MAX_WORKERS))
+            .format(MULTIPROCESSING_MAX_AUTO_WORKERS))
         parser.add_argument('-v', '--verbose', action='store_true',
                             help='write test outputs to stdout.  '
                             'Mostly useful when inspecting a single test')
diff -pruN 3.24.0-1/ase/test/transport/test_transport_calculator.py 3.26.0-1/ase/test/transport/test_transport_calculator.py
--- 3.24.0-1/ase/test/transport/test_transport_calculator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/transport/test_transport_calculator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 # flake8: noqa
 import numpy as np
 
diff -pruN 3.24.0-1/ase/test/utils.py 3.26.0-1/ase/test/utils.py
--- 3.24.0-1/ase/test/utils.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/utils.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from numpy.random import RandomState
 
 
diff -pruN 3.24.0-1/ase/test/vibrations/test_albrecht.py 3.26.0-1/ase/test/vibrations/test_albrecht.py
--- 3.24.0-1/ase/test/vibrations/test_albrecht.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/vibrations/test_albrecht.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.calculators.h2morse import (
diff -pruN 3.24.0-1/ase/test/vibrations/test_bond_polarizability_raman.py 3.26.0-1/ase/test/vibrations/test_bond_polarizability_raman.py
--- 3.24.0-1/ase/test/vibrations/test_bond_polarizability_raman.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/vibrations/test_bond_polarizability_raman.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from pytest import approx, fixture
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/test/vibrations/test_combine.py 3.26.0-1/ase/test/vibrations/test_combine.py
--- 3.24.0-1/ase/test/vibrations/test_combine.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/vibrations/test_combine.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 from ase.build import molecule
 from ase.test.utils import RandomCalculator
 from ase.utils import workdir
diff -pruN 3.24.0-1/ase/test/vibrations/test_folding.py 3.26.0-1/ase/test/vibrations/test_folding.py
--- 3.24.0-1/ase/test/vibrations/test_folding.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/vibrations/test_folding.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pytest
 
 from ase.build import molecule
diff -pruN 3.24.0-1/ase/test/vibrations/test_franck_condon.py 3.26.0-1/ase/test/vibrations/test_franck_condon.py
--- 3.24.0-1/ase/test/vibrations/test_franck_condon.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/vibrations/test_franck_condon.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import sys
 from math import factorial
 
diff -pruN 3.24.0-1/ase/test/vibrations/test_pickle2json.py 3.26.0-1/ase/test/vibrations/test_pickle2json.py
--- 3.24.0-1/ase/test/vibrations/test_pickle2json.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/vibrations/test_pickle2json.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import pickle
 
 import pytest
diff -pruN 3.24.0-1/ase/test/vibrations/test_placzek.py 3.26.0-1/ase/test/vibrations/test_placzek.py
--- 3.24.0-1/ase/test/vibrations/test_placzek.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/vibrations/test_placzek.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 Test Placzek type resonant Raman implementations
 """
diff -pruN 3.24.0-1/ase/test/vibrations/test_profeta_albrecht.py 3.26.0-1/ase/test/vibrations/test_profeta_albrecht.py
--- 3.24.0-1/ase/test/vibrations/test_profeta_albrecht.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/vibrations/test_profeta_albrecht.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """
 Test Placzek and Albrecht resonant Raman implementations
 """
diff -pruN 3.24.0-1/ase/test/vibrations/test_vib.py 3.26.0-1/ase/test/vibrations/test_vib.py
--- 3.24.0-1/ase/test/vibrations/test_vib.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/vibrations/test_vib.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 """Test the ase.vibrations.Vibrations object using a harmonic calculator."""
 import os
 from pathlib import Path
diff -pruN 3.24.0-1/ase/test/vibrations/test_vibrations_example.py 3.26.0-1/ase/test/vibrations/test_vibrations_example.py
--- 3.24.0-1/ase/test/vibrations/test_vibrations_example.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/test/vibrations/test_vibrations_example.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,4 @@
+# fmt: off
 import io
 
 from ase import Atoms
diff -pruN 3.24.0-1/ase/thermochemistry.py 3.26.0-1/ase/thermochemistry.py
--- 3.24.0-1/ase/thermochemistry.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/thermochemistry.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Modules for calculating thermochemical information from computational
 outputs."""
 
@@ -6,6 +8,7 @@ import sys
 from warnings import warn
 
 import numpy as np
+from scipy.integrate import trapezoid
 
 from ase import units
 
@@ -704,18 +707,18 @@ class CrystalThermo(ThermoChem):
 
         zpe_list = omega_e / 2.
         if self.formula_units == 0:
-            zpe = np.trapz(zpe_list * dos_e, omega_e)
+            zpe = trapezoid(zpe_list * dos_e, omega_e)
         else:
-            zpe = np.trapz(zpe_list * dos_e, omega_e) / self.formula_units
+            zpe = trapezoid(zpe_list * dos_e, omega_e) / self.formula_units
         write(fmt % ('E_ZPE', zpe))
         U += zpe
 
         B = 1. / (units.kB * temperature)
         E_vib = omega_e / (np.exp(omega_e * B) - 1.)
         if self.formula_units == 0:
-            E_phonon = np.trapz(E_vib * dos_e, omega_e)
+            E_phonon = trapezoid(E_vib * dos_e, omega_e)
         else:
-            E_phonon = np.trapz(E_vib * dos_e, omega_e) / self.formula_units
+            E_phonon = trapezoid(E_vib * dos_e, omega_e) / self.formula_units
         write(fmt % ('E_phonon', E_phonon))
         U += E_phonon
 
@@ -750,9 +753,9 @@ class CrystalThermo(ThermoChem):
         S_vib = (omega_e / (temperature * (np.exp(omega_e * B) - 1.)) -
                  units.kB * np.log(1. - np.exp(-omega_e * B)))
         if self.formula_units == 0:
-            S = np.trapz(S_vib * dos_e, omega_e)
+            S = trapezoid(S_vib * dos_e, omega_e)
         else:
-            S = np.trapz(S_vib * dos_e, omega_e) / self.formula_units
+            S = trapezoid(S_vib * dos_e, omega_e) / self.formula_units
 
         write('-' * 49)
         write(fmt % ('S', S, S * temperature))
diff -pruN 3.24.0-1/ase/transport/calculators.py 3.26.0-1/ase/transport/calculators.py
--- 3.24.0-1/ase/transport/calculators.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/transport/calculators.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,5 +1,8 @@
+# fmt: off
+
 import numpy as np
 from numpy import linalg
+from scipy.integrate import trapezoid
 
 from ase.transport.greenfunction import GreenFunction
 from ase.transport.selfenergy import BoxProbe, LeadSelfEnergy
@@ -368,9 +371,9 @@ class TransportCalculator:
         fr = fermidistribution(E + bias / 2., kB * T)
 
         if spinpol:
-            return .5 * np.trapz((fl - fr) * T_e, x=E, axis=0)
+            return .5 * trapezoid((fl - fr) * T_e, x=E, axis=0)
         else:
-            return np.trapz((fl - fr) * T_e, x=E, axis=0)
+            return trapezoid((fl - fr) * T_e, x=E, axis=0)
 
     def get_transmission(self):
         self.initialize()
diff -pruN 3.24.0-1/ase/transport/greenfunction.py 3.26.0-1/ase/transport/greenfunction.py
--- 3.24.0-1/ase/transport/greenfunction.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/transport/greenfunction.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 
diff -pruN 3.24.0-1/ase/transport/selfenergy.py 3.26.0-1/ase/transport/selfenergy.py
--- 3.24.0-1/ase/transport/selfenergy.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/transport/selfenergy.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 
diff -pruN 3.24.0-1/ase/transport/stm.py 3.26.0-1/ase/transport/stm.py
--- 3.24.0-1/ase/transport/stm.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/transport/stm.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,7 +1,10 @@
+# fmt: off
+
 # flake8: noqa
 import time
 
 import numpy as np
+from scipy.integrate import trapezoid
 
 from ase.parallel import world
 from ase.transport.greenfunction import GreenFunction
@@ -203,5 +206,5 @@ class STM:
         if i2 < i1:
             step = -1
 
-        return np.sign(bias) * \
-            np.trapz(x=energies[i1:i2:step], y=T_e[i1:i2:step])
+        return np.sign(bias) * trapezoid(x=energies[i1:i2:step],
+                                         y=T_e[i1:i2:step])
diff -pruN 3.24.0-1/ase/transport/tools.py 3.26.0-1/ase/transport/tools.py
--- 3.24.0-1/ase/transport/tools.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/transport/tools.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 
diff -pruN 3.24.0-1/ase/units.py 3.26.0-1/ase/units.py
--- 3.24.0-1/ase/units.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/units.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """ase.units
 
 Physical constants and units derived from CODATA for converting
diff -pruN 3.24.0-1/ase/utils/__init__.py 3.26.0-1/ase/utils/__init__.py
--- 3.24.0-1/ase/utils/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -18,12 +18,32 @@ import numpy as np
 
 from ase.formula import formula_hill, formula_metal
 
-__all__ = ['basestring', 'import_module', 'seterr', 'plural',
-           'devnull', 'gcd', 'convert_string_to_fd', 'Lock',
-           'opencew', 'OpenLock', 'rotate', 'irotate', 'pbc2pbc', 'givens',
-           'hsv2rgb', 'hsv', 'pickleload', 'reader',
-           'formula_hill', 'formula_metal', 'PurePath', 'xwopen',
-           'tokenize_version', 'get_python_package_path_description']
+__all__ = [
+    'basestring',
+    'import_module',
+    'seterr',
+    'plural',
+    'devnull',
+    'gcd',
+    'convert_string_to_fd',
+    'Lock',
+    'opencew',
+    'OpenLock',
+    'rotate',
+    'irotate',
+    'pbc2pbc',
+    'givens',
+    'hsv2rgb',
+    'hsv',
+    'pickleload',
+    'reader',
+    'formula_hill',
+    'formula_metal',
+    'PurePath',
+    'xwopen',
+    'tokenize_version',
+    'get_python_package_path_description',
+]
 
 
 def tokenize_version(version_string: str):
@@ -52,7 +72,7 @@ pickleload = functools.partial(pickle.lo
 def deprecated(
     message: Union[str, Warning],
     category: Type[Warning] = FutureWarning,
-    callback: Callable[[List, Dict], bool] = lambda args, kwargs: True
+    callback: Callable[[List, Dict], bool] = lambda args, kwargs: True,
 ):
     """Return a decorator deprecating a function.
 
@@ -167,8 +187,9 @@ class DevNull:
     encoding = 'UTF-8'
     closed = False
 
-    _use_os_devnull = deprecated('use open(os.devnull) instead',
-                                 DeprecationWarning)
+    _use_os_devnull = deprecated(
+        'use open(os.devnull) instead', DeprecationWarning
+    )
     # Deprecated for ase-3.21.0.  Change to futurewarning later on.
 
     @_use_os_devnull
@@ -203,9 +224,11 @@ class DevNull:
 devnull = DevNull()
 
 
-@deprecated('convert_string_to_fd does not facilitate proper resource '
-            'management.  '
-            'Please use e.g. ase.utils.IOContext class instead.')
+@deprecated(
+    'convert_string_to_fd does not facilitate proper resource '
+    'management.  '
+    'Please use e.g. ase.utils.IOContext class instead.'
+)
 def convert_string_to_fd(name, world=None):
     """Create a file-descriptor for text output.
 
@@ -254,6 +277,7 @@ def opencew(filename, world=None):
 
 def _opencew(filename, world=None):
     import ase.parallel as parallel
+
     if world is None:
         world = parallel.world
 
@@ -408,31 +432,27 @@ def rotate(rotations, rotation=np.identi
     if rotations == '':
         return rotation.copy()
 
-    for i, a in [('xyz'.index(s[-1]), radians(float(s[:-1])))
-                 for s in rotations.split(',')]:
+    for i, a in [
+        ('xyz'.index(s[-1]), radians(float(s[:-1])))
+        for s in rotations.split(',')
+    ]:
         s = sin(a)
         c = cos(a)
         if i == 0:
-            rotation = np.dot(rotation, [(1, 0, 0),
-                                         (0, c, s),
-                                         (0, -s, c)])
+            rotation = np.dot(rotation, [(1, 0, 0), (0, c, s), (0, -s, c)])
         elif i == 1:
-            rotation = np.dot(rotation, [(c, 0, -s),
-                                         (0, 1, 0),
-                                         (s, 0, c)])
+            rotation = np.dot(rotation, [(c, 0, -s), (0, 1, 0), (s, 0, c)])
         else:
-            rotation = np.dot(rotation, [(c, s, 0),
-                                         (-s, c, 0),
-                                         (0, 0, 1)])
+            rotation = np.dot(rotation, [(c, s, 0), (-s, c, 0), (0, 0, 1)])
     return rotation
 
 
 def givens(a, b):
     """Solve the equation system::
 
-      [ c s]   [a]   [r]
-      [    ] . [ ] = [ ]
-      [-s c]   [b]   [0]
+    [ c s]   [a]   [r]
+    [    ] . [ ] = [ ]
+    [-s c]   [b]   [0]
     """
     sgn = np.sign
     if b == 0:
@@ -441,14 +461,14 @@ def givens(a, b):
         r = abs(a)
     elif abs(b) >= abs(a):
         cot = a / b
-        u = sgn(b) * (1 + cot**2)**0.5
-        s = 1. / u
+        u = sgn(b) * (1 + cot**2) ** 0.5
+        s = 1.0 / u
         c = s * cot
         r = b * u
     else:
         tan = b / a
-        u = sgn(a) * (1 + tan**2)**0.5
-        c = 1. / u
+        u = sgn(a) * (1 + tan**2) ** 0.5
+        c = 1.0 / u
         s = c * tan
         r = a * u
     return c, s, r
@@ -459,8 +479,10 @@ def irotate(rotation, initial=np.identit
     a = np.dot(initial, rotation)
     cx, sx, rx = givens(a[2, 2], a[1, 2])
     cy, sy, _ry = givens(rx, a[0, 2])
-    cz, sz, _rz = givens(cx * a[1, 1] - sx * a[2, 1],
-                        cy * a[0, 1] - sy * (sx * a[1, 1] + cx * a[2, 1]))
+    cz, sz, _rz = givens(
+        cx * a[1, 1] - sx * a[2, 1],
+        cy * a[0, 1] - sy * (sx * a[1, 1] + cx * a[2, 1]),
+    )
     x = degrees(atan2(sx, cx))
     y = degrees(atan2(-sy, cy))
     z = degrees(atan2(sz, cz))
@@ -499,7 +521,7 @@ def hsv2rgb(h, s, v):
     if s == 0:
         return v, v, v
 
-    i, f = divmod(h / 60., 1)
+    i, f = divmod(h / 60.0, 1)
     p = v * (1 - s)
     q = v * (1 - s * f)
     t = v * (1 - s * (1 - f))
@@ -520,8 +542,8 @@ def hsv2rgb(h, s, v):
         raise RuntimeError('h must be in [0, 360]')
 
 
-def hsv(array, s=.9, v=.9):
-    array = (array + array.min()) * 359. / (array.max() - array.min())
+def hsv(array, s=0.9, v=0.9):
+    array = (array + array.min()) * 359.0 / (array.max() - array.min())
     result = np.empty((len(array.flat), 3))
     for rgb, h in zip(result, array.flat):
         rgb[:] = hsv2rgb(h, s, v)
@@ -580,6 +602,7 @@ class iofunction:
                 if openandclose and fd is not None:
                     # fd may be None if open() failed
                     fd.close()
+
         return iofunc
 
 
@@ -596,9 +619,11 @@ def reader(func):
 # in ase.io.jsonio, but we'd rather import them from a 'basic' module
 # like ase/utils than one which triggers a lot of extra (cyclic) imports.
 
+
 def write_json(self, fd):
     """Write to JSON file."""
     from ase.io.jsonio import write_json as _write_json
+
     _write_json(fd, self)
 
 
@@ -606,6 +631,7 @@ def write_json(self, fd):
 def read_json(cls, fd):
     """Read new instance from JSON file."""
     from ase.io.jsonio import read_json as _read_json
+
     obj = _read_json(fd)
     assert isinstance(obj, cls)
     return obj
@@ -620,6 +646,7 @@ def jsonable(name):
     (such as ndarray, float, ...) or implement todict().  If the class
     defines a string called ase_objtype, the decoder will want to convert
     the object back into its original type when reading."""
+
     def jsonableclass(cls):
         cls.ase_objtype = name
         if not hasattr(cls, 'todict'):
@@ -633,6 +660,7 @@ def jsonable(name):
         cls.write = write_json
         cls.read = read_json
         return cls
+
     return jsonableclass
 
 
@@ -642,15 +670,21 @@ class ExperimentalFeatureWarning(Warning
 
 def experimental(func):
     """Decorator for functions not ready for production use."""
+
     @functools.wraps(func)
     def expfunc(*args, **kwargs):
-        warnings.warn('This function may change or misbehave: {}()'
-                      .format(func.__qualname__),
-                      ExperimentalFeatureWarning)
+        warnings.warn(
+            'This function may change or misbehave: {}()'.format(
+                func.__qualname__
+            ),
+            ExperimentalFeatureWarning,
+        )
         return func(*args, **kwargs)
+
     return expfunc
 
 
+@deprecated('use functools.cached_property instead')
 def lazymethod(meth):
     """Decorator for lazy evaluation and caching of data.
 
@@ -664,7 +698,10 @@ def lazymethod(meth):
 
     The method body is only executed first time thing() is called, and
     its return value is stored.  Subsequent calls return the cached
-    value."""
+    value.
+
+    .. deprecated:: 3.25.0
+    """
     name = meth.__name__
 
     @functools.wraps(meth)
@@ -677,14 +714,17 @@ def lazymethod(meth):
         if name not in cache:
             cache[name] = meth(self)
         return cache[name]
+
     return getter
 
 
 def atoms_to_spglib_cell(atoms):
     """Convert atoms into data suitable for calling spglib."""
-    return (atoms.get_cell(),
-            atoms.get_scaled_positions(),
-            atoms.get_atomic_numbers())
+    return (
+        atoms.get_cell(),
+        atoms.get_scaled_positions(),
+        atoms.get_atomic_numbers(),
+    )
 
 
 def warn_legacy(feature_name):
@@ -692,18 +732,32 @@ def warn_legacy(feature_name):
         f'The {feature_name} feature is untested and ASE developers do not '
         'know whether it works or how to use it.  Please rehabilitate it '
         '(by writing unittests) or it may be removed.',
-        FutureWarning)
+        FutureWarning,
+    )
 
 
+@deprecated('use functools.cached_property instead')
 def lazyproperty(meth):
-    """Decorator like lazymethod, but making item available as a property."""
+    """Decorator like lazymethod, but making item available as a property.
+
+    .. deprecated:: 3.25.0
+    """
     return property(lazymethod(meth))
 
 
+class _DelExitStack(ExitStack):
+    # We don't want IOContext itself to implement __del__, since IOContext
+    # might be subclassed, and we don't want __del__ on objects that we
+    # don't fully control.  Therefore we make a little custom class
+    # that nobody else refers to, and that has the __del__.
+    def __del__(self):
+        self.close()
+
+
 class IOContext:
-    @lazyproperty
+    @functools.cached_property
     def _exitstack(self):
-        return ExitStack()
+        return _DelExitStack()
 
     def __enter__(self):
         return self
@@ -724,8 +778,9 @@ class IOContext:
         encoding = None if mode.endswith('b') else 'utf-8'
 
         if file is None or comm.rank != 0:
-            return self.closelater(open(os.devnull, mode=mode,
-                                        encoding=encoding))
+            return self.closelater(
+                open(os.devnull, mode=mode, encoding=encoding)
+            )
 
         if file == '-':
             return sys.stdout
@@ -734,7 +789,8 @@ class IOContext:
 
 
 def get_python_package_path_description(
-        package, default='module has no path') -> str:
+    package, default='module has no path'
+) -> str:
     """Helper to get path description of a python package/module
 
     If path has multiple elements, the first one is returned.
@@ -749,4 +805,4 @@ def get_python_package_path_description(
         else:
             return default
     except Exception as ex:
-        return f"{default} ({ex})"
+        return f'{default} ({ex})'
diff -pruN 3.24.0-1/ase/utils/abc.py 3.26.0-1/ase/utils/abc.py
--- 3.24.0-1/ase/utils/abc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/abc.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,5 +1,4 @@
-import collections
-from abc import abstractmethod
+from abc import ABC, abstractmethod
 
 import numpy as np
 
@@ -8,32 +7,54 @@ import numpy as np
 # Can we find a better way?
 
 
-class Optimizable(collections.abc.Sized):
+class Optimizable(ABC):
     @abstractmethod
-    def get_positions(self):
-        ...
+    def ndofs(self) -> int:
+        """Return number of degrees of freedom."""
 
     @abstractmethod
-    def set_positions(self, positions):
-        ...
+    def get_x(self) -> np.ndarray:
+        """Return current coordinates as a flat ndarray."""
 
     @abstractmethod
-    def get_forces(self):
-        ...
+    def set_x(self, x: np.ndarray) -> None:
+        """Set flat ndarray as current coordinates."""
 
     @abstractmethod
-    def get_potential_energy(self):
-        ...
+    def get_gradient(self) -> np.ndarray:
+        """Return gradient at current coordinates as flat ndarray.
+
+        NOTE: Currently this is still the (flat) "forces" i.e.
+        the negative gradient.  This must be fixed before the optimizable
+        API is done."""
+        # Callers who want Nx3 will do ".get_gradient().reshape(-1, 3)".
+        # We can probably weed out most such reshapings.
+        # Grep for the above expression in order to find places that should
+        # be updated.
+
+    @abstractmethod
+    def get_value(self) -> float:
+        """Return function value at current coordinates."""
 
     @abstractmethod
     def iterimages(self):
-        ...
+        """Yield domain objects that can be saved as trajectory.
+
+        For example this can yield Atoms objects if the optimizer
+        has a trajectory that can write Atoms objects."""
+
+    def converged(self, gradient: np.ndarray, fmax: float) -> bool:
+        """Standard implementation of convergence criterion.
 
-    def converged(self, forces, fmax):
-        return np.linalg.norm(forces, axis=1).max() < fmax
+        This assumes that forces are the actual (Nx3) forces.
+        We can hopefully change this."""
+        assert gradient.ndim == 1
+        return self.gradient_norm(gradient) < fmax
 
-    def is_neb(self):
-        return False
+    def gradient_norm(self, gradient):
+        forces = gradient.reshape(-1, 3)  # XXX Cartesian
+        return np.linalg.norm(forces, axis=1).max()
 
-    def __ase_optimizable__(self):
+    def __ase_optimizable__(self) -> 'Optimizable':
+        """Return self, being already an Optimizable."""
         return self
diff -pruN 3.24.0-1/ase/utils/arraywrapper.py 3.26.0-1/ase/utils/arraywrapper.py
--- 3.24.0-1/ase/utils/arraywrapper.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/arraywrapper.py	2025-08-12 11:26:23.000000000 +0000
@@ -22,24 +22,56 @@ This module provides the @arraylike deco
 for all the interesting ndarray methods.
 """
 
-
 from functools import update_wrapper
 
 import numpy as np
 
-inplace_methods = ['__iadd__', '__imul__', '__ipow__', '__isub__',
-                   '__itruediv__', '__imatmul__']
-
-forward_methods = ['__abs__', '__add__', '__contains__', '__eq__',
-                   '__ge__', '__getitem__', '__gt__', '__hash__',
-                   '__iter__', '__le__', '__len__', '__lt__',
-                   '__mul__', '__ne__', '__neg__', '__pos__',
-                   '__pow__', '__radd__', '__rmul__', '__rpow__',
-                   '__rsub__', '__rtruediv__', '__setitem__',
-                   '__sub__', '__truediv__']
-
-default_methods = ['__eq__', '__le__', '__lt__', '__ge__',
-                   '__gt__', '__ne__', '__hash__']
+inplace_methods = [
+    '__iadd__',
+    '__imul__',
+    '__ipow__',
+    '__isub__',
+    '__itruediv__',
+    '__imatmul__',
+]
+
+forward_methods = [
+    '__abs__',
+    '__add__',
+    '__contains__',
+    '__eq__',
+    '__ge__',
+    '__getitem__',
+    '__gt__',
+    '__hash__',
+    '__iter__',
+    '__le__',
+    '__len__',
+    '__lt__',
+    '__mul__',
+    '__ne__',
+    '__neg__',
+    '__pos__',
+    '__pow__',
+    '__radd__',
+    '__rmul__',
+    '__rpow__',
+    '__rsub__',
+    '__rtruediv__',
+    '__setitem__',
+    '__sub__',
+    '__truediv__',
+]
+
+default_methods = [
+    '__eq__',
+    '__le__',
+    '__lt__',
+    '__ge__',
+    '__gt__',
+    '__ne__',
+    '__hash__',
+]
 
 if hasattr(np.ndarray, '__matmul__'):
     forward_methods += ['__matmul__', '__rmatmul__']
diff -pruN 3.24.0-1/ase/utils/build_web_page.py 3.26.0-1/ase/utils/build_web_page.py
--- 3.24.0-1/ase/utils/build_web_page.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/build_web_page.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,63 +0,0 @@
-"""Build ASE's web-page."""
-
-import os
-import shutil
-import subprocess
-import sys
-from pathlib import Path
-
-cmds = """\
-. /etc/bashrc
-module load Tkinter
-python3 -m venv venv
-. venv/bin/activate
-pip install -U pip
-pip install "sphinx<6.0"  # search broken in sphinx-6
-pip install sphinx-rtd-theme pillow
-git clone http://gitlab.com/ase/ase.git
-cd ase
-pip install .
-python setup.py sdist
-cd doc
-make
-mv build/html ase-web-page"""
-
-
-def build():
-    root = Path('/tmp/ase-docs')
-    if root.is_dir():
-        sys.exit('Locked')
-    root.mkdir()
-    os.chdir(root)
-    cmds2 = ' && '.join(line.split('#')[0] for line in cmds.splitlines())
-    p = subprocess.run(cmds2, shell=True, check=False)
-    if p.returncode == 0:
-        status = 'ok'
-    else:
-        print('FAILED!', file=sys.stdout)
-        status = 'error'
-    f = root.with_name(f'ase-docs-{status}')
-    if f.is_dir():
-        shutil.rmtree(f)
-    root.rename(f)
-    return status
-
-
-def build_all():
-    assert build() == 'ok'
-    tar = next(
-        Path('/tmp/ase-docs-ok/ase/dist/').glob('ase-*.tar.gz'))
-    webpage = Path('/tmp/ase-docs-ok/ase/doc/ase-web-page')
-    home = Path.home() / 'web-pages'
-    cmds = ' && '.join(
-        [f'cp {tar} {webpage}',
-         f'find {webpage} -name install.html | '
-         f'xargs sed -i s/snapshot.tar.gz/{tar.name}/g',
-         f'cd {webpage.parent}',
-         'tar -czf ase-web-page.tar.gz ase-web-page',
-         f'cp ase-web-page.tar.gz {home}'])
-    subprocess.run(cmds, shell=True, check=True)
-
-
-if __name__ == '__main__':
-    build_all()
diff -pruN 3.24.0-1/ase/utils/checkimports.py 3.26.0-1/ase/utils/checkimports.py
--- 3.24.0-1/ase/utils/checkimports.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/checkimports.py	2025-08-12 11:26:23.000000000 +0000
@@ -9,6 +9,7 @@ See https://gitlab.com/ase/ase/-/issues/
 The utility here is general, so it can be used for checking and
 monitoring other code snippets too.
 """
+
 import json
 import os
 import re
@@ -33,23 +34,31 @@ def exec_and_check_modules(expression: s
     # Take null outside command to avoid
     # `import os` before expression
     null = os.devnull
-    command = ("import sys;"
-               f" stdout = sys.stdout; sys.stdout = open({null!r}, 'w');"
-               f" {expression};"
-               " sys.stdout = stdout;"
-               " modules = list(sys.modules);"
-               " import json; print(json.dumps(modules))")
-    proc = run([sys.executable, '-c', command],
-               capture_output=True, universal_newlines=True,
-               check=True)
+    command = (
+        'import sys;'
+        f" stdout = sys.stdout; sys.stdout = open({null!r}, 'w');"
+        f' {expression};'
+        ' sys.stdout = stdout;'
+        ' modules = list(sys.modules);'
+        ' import json; print(json.dumps(modules))'
+    )
+    proc = run(
+        [sys.executable, '-c', command],
+        capture_output=True,
+        universal_newlines=True,
+        check=True,
+    )
     return set(json.loads(proc.stdout))
 
 
-def check_imports(expression: str, *,
-                  forbidden_modules: List[str] = [],
-                  max_module_count: Optional[int] = None,
-                  max_nonstdlib_module_count: Optional[int] = None,
-                  do_print: bool = False) -> None:
+def check_imports(
+    expression: str,
+    *,
+    forbidden_modules: List[str] = [],
+    max_module_count: Optional[int] = None,
+    max_nonstdlib_module_count: Optional[int] = None,
+    do_print: bool = False,
+) -> None:
     """Check modules imported by the execution of a Python expression.
 
     Parameters
@@ -74,8 +83,7 @@ def check_imports(expression: str, *,
     for module_pattern in forbidden_modules:
         r = re.compile(module_pattern)
         for module in modules:
-            assert not r.fullmatch(module), \
-                f'{module} was imported'
+            assert not r.fullmatch(module), f'{module} was imported'
 
     if max_nonstdlib_module_count is not None:
         assert sys.version_info >= (3, 10), 'Python 3.10+ required'
@@ -83,8 +91,7 @@ def check_imports(expression: str, *,
         nonstdlib_modules = []
         for module in modules:
             if (
-                module.split('.')[0]
-                in sys.stdlib_module_names  # type: ignore[attr-defined]
+                module.split('.')[0] in sys.stdlib_module_names  # type: ignore[attr-defined]
             ):
                 continue
             nonstdlib_modules.append(module)
@@ -101,8 +108,9 @@ def check_imports(expression: str, *,
 
     if max_module_count is not None:
         module_count = len(modules)
-        assert module_count <= max_module_count, \
+        assert module_count <= max_module_count, (
             f'too many modules loaded: {module_count}/{max_module_count}'
+        )
 
 
 if __name__ == '__main__':
diff -pruN 3.24.0-1/ase/utils/cube.py 3.26.0-1/ase/utils/cube.py
--- 3.24.0-1/ase/utils/cube.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/cube.py	2025-08-12 11:26:23.000000000 +0000
@@ -2,8 +2,16 @@ import numpy as np
 from scipy.interpolate import interpn
 
 
-def grid_2d_slice(spacings, array, u, v, o=(0, 0, 0), step=0.02,
-                  size_u=(-10, 10), size_v=(-10, 10)):
+def grid_2d_slice(
+    spacings,
+    array,
+    u,
+    v,
+    o=(0, 0, 0),
+    step=0.02,
+    size_u=(-10, 10),
+    size_v=(-10, 10),
+):
     """Extract a 2D slice from a cube file using interpolation.
 
     Works for non-orthogonal cells.
@@ -97,14 +105,15 @@ def grid_2d_slice(spacings, array, u, v,
     B = np.array([u, u_perp, n])
     Bo = np.dot(B, o)
 
-    det = (u[0] * v[1] - v[0] * u[1])
+    det = u[0] * v[1] - v[0] * u[1]
 
     if det == 0:
         zoff = 0
     else:
-        zoff = ((0 - o[1]) * (u[0] * v[2] - v[0] * u[2]) -
-                (0 - o[0]) * (u[1] * v[2] - v[1] * u[2])) \
-            / det + o[2]
+        zoff = (
+            (0 - o[1]) * (u[0] * v[2] - v[0] * u[2])
+            - (0 - o[0]) * (u[1] * v[2] - v[1] * u[2])
+        ) / det + o[2]
 
     zoff = np.dot(B, [0, 0, zoff])[-1]
 
@@ -124,11 +133,8 @@ def grid_2d_slice(spacings, array, u, v,
     # We avoid nan values at boundary
     vectors = np.round(vectors, 12)
 
-    D = interpn((ox, oy, oz),
-                array,
-                vectors,
-                bounds_error=False,
-                method='linear'
-                ).reshape(X.shape)
+    D = interpn(
+        (ox, oy, oz), array, vectors, bounds_error=False, method='linear'
+    ).reshape(X.shape)
 
     return X - Bo[0], Y - Bo[1], D
diff -pruN 3.24.0-1/ase/utils/deltacodesdft.py 3.26.0-1/ase/utils/deltacodesdft.py
--- 3.24.0-1/ase/utils/deltacodesdft.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/deltacodesdft.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,9 +3,15 @@ import numpy as np
 from ase.eos import birchmurnaghan
 
 
-def delta(v1: float, B1: float, Bp1: float,
-          v2: float, B2: float, Bp2: float,
-          symmetric=True) -> float:
+def delta(
+    v1: float,
+    B1: float,
+    Bp1: float,
+    v2: float,
+    B2: float,
+    Bp2: float,
+    symmetric=True,
+) -> float:
     """Calculate Delta-value between two equation of states.
 
     .. seealso:: https://github.com/molmod/DeltaCodesDFT
@@ -37,4 +43,4 @@ def delta(v1: float, B1: float, Bp1: flo
     v = np.linspace(va + dv / 2, vb - dv / 2, npoints)
     e1 = birchmurnaghan(v, 0.0, B1, Bp1, v1)
     e2 = birchmurnaghan(v, 0.0, B2, Bp2, v2)
-    return (((e1 - e2)**2).sum() * dv / (vb - va))**0.5
+    return (((e1 - e2) ** 2).sum() * dv / (vb - va)) ** 0.5
diff -pruN 3.24.0-1/ase/utils/ff.py 3.26.0-1/ase/utils/ff.py
--- 3.24.0-1/ase/utils/ff.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/ff.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 import numpy as np
 from numpy import linalg
diff -pruN 3.24.0-1/ase/utils/filecache.py 3.26.0-1/ase/utils/filecache.py
--- 3.24.0-1/ase/utils/filecache.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/filecache.py	2025-08-12 11:26:23.000000000 +0000
@@ -181,8 +181,9 @@ class _MultiFileCacheTemplate(MutableMap
             missing(key)
 
     def combine(self):
-        cache = self.backend.dump_cache(self.directory, dict(self),
-                                        comm=self.comm)
+        cache = self.backend.dump_cache(
+            self.directory, dict(self), comm=self.comm
+        )
         assert set(cache) == set(self)
         self.clear()
         assert len(self) == 0
@@ -254,8 +255,9 @@ class _CombinedCacheTemplate(Mapping):
         return self
 
     def split(self):
-        cache = self.backend.create_multifile_cache(self.directory,
-                                                    comm=self.comm)
+        cache = self.backend.create_multifile_cache(
+            self.directory, comm=self.comm
+        )
         assert len(cache) == 0
         cache.update(self)
         assert set(cache) == set(self)
diff -pruN 3.24.0-1/ase/utils/forcecurve.py 3.26.0-1/ase/utils/forcecurve.py
--- 3.24.0-1/ase/utils/forcecurve.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/forcecurve.py	2025-08-12 11:26:23.000000000 +0000
@@ -43,15 +43,20 @@ def fit_raw(energies, forces, positions,
             s0 = path[i - 1]
             s1 = path[i]
             x = np.linspace(s0, s1, 20, endpoint=False)
-            c = np.linalg.solve(np.array([(1, s0, s0**2, s0**3),
-                                          (1, s1, s1**2, s1**3),
-                                          (0, 1, 2 * s0, 3 * s0**2),
-                                          (0, 1, 2 * s1, 3 * s1**2)]),
-                                np.array([energies[i - 1], energies[i],
-                                          lastslope, slope]))
+            c = np.linalg.solve(
+                np.array(
+                    [
+                        (1, s0, s0**2, s0**3),
+                        (1, s1, s1**2, s1**3),
+                        (0, 1, 2 * s0, 3 * s0**2),
+                        (0, 1, 2 * s1, 3 * s1**2),
+                    ]
+                ),
+                np.array([energies[i - 1], energies[i], lastslope, slope]),
+            )
             y = c[0] + x * (c[1] + x * (c[2] + x * c[3]))
-            fit_path[(i - 1) * 20:i * 20] = x
-            fit_energies[(i - 1) * 20:i * 20] = y
+            fit_path[(i - 1) * 20 : i * 20] = x
+            fit_energies[(i - 1) * 20 : i * 20] = y
 
         lastslope = slope
 
@@ -60,12 +65,16 @@ def fit_raw(energies, forces, positions,
     return ForceFit(path, energies, fit_path, fit_energies, lines)
 
 
-class ForceFit(namedtuple('ForceFit', ['path', 'energies', 'fit_path',
-                                       'fit_energies', 'lines'])):
+class ForceFit(
+    namedtuple(
+        'ForceFit', ['path', 'energies', 'fit_path', 'fit_energies', 'lines']
+    )
+):
     """Data container to hold fitting parameters for force curves."""
 
     def plot(self, ax=None):
         import matplotlib.pyplot as plt
+
         if ax is None:
             ax = plt.gca()
 
@@ -78,9 +87,11 @@ class ForceFit(namedtuple('ForceFit', ['
         Ef = max(self.energies) - self.energies[0]
         Er = max(self.energies) - self.energies[-1]
         dE = self.energies[-1] - self.energies[0]
-        ax.set_title(r'$E_\mathrm{{f}} \approx$ {:.3f} eV; '
-                     r'$E_\mathrm{{r}} \approx$ {:.3f} eV; '
-                     r'$\Delta E$ = {:.3f} eV'.format(Ef, Er, dE))
+        ax.set_title(
+            r'$E_\mathrm{{f}} \approx$ {:.3f} eV; '
+            r'$E_\mathrm{{r}} \approx$ {:.3f} eV; '
+            r'$\Delta E$ = {:.3f} eV'.format(Ef, Er, dE)
+        )
         return ax
 
 
@@ -105,6 +116,7 @@ def force_curve(images, ax=None):
 
     if ax is None:
         import matplotlib.pyplot as plt
+
         ax = plt.gca()
 
     nim = len(images)
@@ -114,8 +126,7 @@ def force_curve(images, ax=None):
 
     # XXX force_consistent=True will work with some calculators,
     # but won't work if images were loaded from a trajectory.
-    energies = [atoms.get_potential_energy()
-                for atoms in images]
+    energies = [atoms.get_potential_energy() for atoms in images]
 
     for i in range(nim):
         atoms = images[i]
@@ -131,11 +142,12 @@ def force_curve(images, ax=None):
         else:
             leftpos = atoms.positions
 
-        disp_ac, _ = find_mic(rightpos - leftpos, cell=atoms.cell,
-                              pbc=atoms.pbc)
+        disp_ac, _ = find_mic(
+            rightpos - leftpos, cell=atoms.cell, pbc=atoms.pbc
+        )
 
         def total_displacement(disp):
-            disp_a = (disp**2).sum(axis=1)**.5
+            disp_a = (disp**2).sum(axis=1) ** 0.5
             return sum(disp_a)
 
         dE_fdotr = -0.5 * np.vdot(f_ac.ravel(), disp_ac.ravel())
@@ -166,11 +178,13 @@ def force_curve(images, ax=None):
 
 def plotfromfile(*fnames):
     from ase.io import read
+
     nplots = len(fnames)
 
     for i, fname in enumerate(fnames):
         images = read(fname, ':')
         import matplotlib.pyplot as plt
+
         plt.subplot(nplots, 1, 1 + i)
         force_curve(images)
     plt.show()
@@ -178,5 +192,6 @@ def plotfromfile(*fnames):
 
 if __name__ == '__main__':
     import sys
+
     fnames = sys.argv[1:]
     plotfromfile(*fnames)
diff -pruN 3.24.0-1/ase/utils/linesearch.py 3.26.0-1/ase/utils/linesearch.py
--- 3.24.0-1/ase/utils/linesearch.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/linesearch.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 import numpy as np
 
diff -pruN 3.24.0-1/ase/utils/linesearcharmijo.py 3.26.0-1/ase/utils/linesearcharmijo.py
--- 3.24.0-1/ase/utils/linesearcharmijo.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/linesearcharmijo.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,28 +1,17 @@
-# flake8: noqa
 import logging
 import math
 
 import numpy as np
-
-# CO <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
-try:
-    import scipy
-    import scipy.linalg
-    have_scipy = True
-except ImportError:
-    have_scipy = False
-# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
+import scipy
+import scipy.linalg
 
 from ase.utils import longsum
 
 logger = logging.getLogger(__name__)
 
-# CO <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
-
 
 class LinearPath:
-    """Describes a linear search path of the form t -> t g
-    """
+    """Describes a linear search path of the form t -> t g"""
 
     def __init__(self, dirn):
         """Initialise LinearPath object
@@ -46,9 +35,9 @@ def nullspace(A, myeps=1e-10):
     """
     u, s, vh = scipy.linalg.svd(A)
     padding = max(0, np.shape(A)[1] - np.shape(s)[0])
-    null_mask = np.concatenate(((s <= myeps),
-                                np.ones((padding,), dtype=bool)),
-                               axis=0)
+    null_mask = np.concatenate(
+        ((s <= myeps), np.ones((padding,), dtype=bool)), axis=0
+    )
     null_space = scipy.compress(null_mask, vh, axis=0)
     return scipy.transpose(null_space)
 
@@ -85,10 +74,6 @@ class RumPath:
                              rigid_units
         """
 
-        if not have_scipy:
-            raise RuntimeError(
-                "RumPath depends on scipy, which could not be imported")
-
         # keep some stuff stored
         self.rotation_factors = rotation_factors
         self.rigid_units = rigid_units
@@ -115,9 +100,13 @@ class RumPath:
             A = np.zeros((3, 3))
             b = np.zeros(3)
             for j in range(len(I)):
-                Yj = np.array([[y[1, j], 0.0, -y[2, j]],
-                               [-y[0, j], y[2, j], 0.0],
-                               [0.0, -y[1, j], y[0, j]]])
+                Yj = np.array(
+                    [
+                        [y[1, j], 0.0, -y[2, j]],
+                        [-y[0, j], y[2, j], 0.0],
+                        [0.0, -y[1, j], y[0, j]],
+                    ]
+                )
                 A += np.dot(Yj.T, Yj)
                 b += np.dot(Yj.T, f[:, j])
             # If the directions y[:,j] span all of R^3 (canonically this is true
@@ -132,9 +121,9 @@ class RumPath:
             b -= np.dot(np.dot(N, N.T), b)
             A += np.dot(N, N.T)
             k = scipy.linalg.solve(A, b, sym_pos=True)
-            K = np.array([[0.0, k[0], -k[2]],
-                          [-k[0], 0.0, k[1]],
-                          [k[2], -k[1], 0.0]])
+            K = np.array(
+                [[0.0, k[0], -k[2]], [-k[0], 0.0, k[1]], [k[2], -k[1], 0.0]]
+            )
             # now remove the rotational component from the search direction
             # ( we actually keep the translational component as part of w,
             #   but this could be changed as well! )
@@ -158,23 +147,23 @@ class RumPath:
         # translation and stretch
         s = alpha * self.stretch
         # loop through rigid_units
-        for (I, K, y, rf) in zip(self.rigid_units, self.K, self.y,
-                                 self.rotation_factors):
+        for I, K, y, rf in zip(
+            self.rigid_units, self.K, self.y, self.rotation_factors
+        ):
             # with matrix exponentials:
             #      s[:, I] += expm(K * alpha * rf) * p.y - p.y
             # third-order taylor approximation:
             #      I + t K + 1/2 t^2 K^2 + 1/6 t^3 K^3 - I
             #                            = t K (I + 1/2 t K (I + 1/3 t K))
             aK = alpha * rf * K
-            s[:, I] += np.dot(aK, y + 0.5 * np.dot(aK,
-                              y + 1 / 3. * np.dot(aK, y)))
+            s[:, I] += np.dot(
+                aK, y + 0.5 * np.dot(aK, y + 1 / 3.0 * np.dot(aK, y))
+            )
 
         return s.ravel()
-# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
 
 
 class LineSearchArmijo:
-
     def __init__(self, func, c1=0.1, tol=1e-14):
         """Initialise the linesearch with set parameters and functions.
 
@@ -190,19 +179,34 @@ class LineSearchArmijo:
         self.func = func
 
         if not (0 < c1 < 0.5):
-            logger.error("c1 outside of allowed interval (0, 0.5). Replacing with "
-                         "default value.")
-            print("Warning: C1 outside of allowed interval. Replacing with "
-                  "default value.")
+            logger.error(
+                'c1 outside of allowed interval (0, 0.5). Replacing with '
+                'default value.'
+            )
+            print(
+                'Warning: C1 outside of allowed interval. Replacing with '
+                'default value.'
+            )
             c1 = 0.1
 
         self.c1 = c1
 
         # CO : added rigid_units and rotation_factors
 
-    def run(self, x_start, dirn, a_max=None, a_min=None, a1=None,
-            func_start=None, func_old=None, func_prime_start=None,
-            rigid_units=None, rotation_factors=None, maxstep=None):
+    def run(
+        self,
+        x_start,
+        dirn,
+        a_max=None,
+        a_min=None,
+        a1=None,
+        func_start=None,
+        func_old=None,
+        func_prime_start=None,
+        rigid_units=None,
+        rotation_factors=None,
+        maxstep=None,
+    ):
         """Perform a backtracking / quadratic-interpolation linesearch
             to find an appropriate step length with Armijo condition.
         NOTE THIS LINESEARCH DOES NOT IMPOSE WOLFE CONDITIONS!
@@ -222,8 +226,10 @@ class LineSearchArmijo:
             dirn: vector pointing in the direction to search in (pk in [NW]).
                 Note that this does not have to be a unit vector, but the
                 function will return a value scaled with respect to dirn.
-            a_max: an upper bound on the maximum step length allowed. Default is 2.0.
-            a_min: a lower bound on the minimum step length allowed. Default is 1e-10.
+            a_max: an upper bound on the maximum step length allowed.
+                Default is 2.0.
+            a_min: a lower bound on the minimum step length allowed.
+                Default is 1e-10.
                 A RuntimeError is raised if this bound is violated
                 during the line search.
             a1: the initial guess for an acceptable step length. If no value is
@@ -238,8 +244,8 @@ class LineSearchArmijo:
             func_old: the value of func_start at the previous step taken in
                 the optimisation (this will be used to calculate the initial
                 guess for the step length if it is not provided)
-            rigid_units, rotationfactors : see documentation of RumPath, if it is
-                unclear what these parameters are, then leave them at None
+            rigid_units, rotationfactors : see documentation of RumPath,if it
+                is unclear what these parameters are, then leave them at None
             maxstep: maximum allowed displacement in Angstrom. Default is 0.2.
 
         Returns:
@@ -256,90 +262,116 @@ class LineSearchArmijo:
             RuntimeError for problems encountered during iteration
         """
 
-        a1 = self.handle_args(x_start, dirn, a_max, a_min, a1, func_start,
-                              func_old, func_prime_start, maxstep)
+        a1 = self.handle_args(
+            x_start,
+            dirn,
+            a_max,
+            a_min,
+            a1,
+            func_start,
+            func_old,
+            func_prime_start,
+            maxstep,
+        )
 
         # DEBUG
-        logger.debug("a1(auto) = %e", a1)
+        logger.debug('a1(auto) = %e', a1)
 
         if abs(a1 - 1.0) <= 0.5:
             a1 = 1.0
 
-        logger.debug("-----------NEW LINESEARCH STARTED---------")
+        logger.debug('-----------NEW LINESEARCH STARTED---------')
 
         a_final = None
         phi_a_final = None
         num_iter = 0
 
-        # CO <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
         # create a search-path
         if rigid_units is None:
             # standard linear search-path
-            logger.debug("-----using LinearPath-----")
+            logger.debug('-----using LinearPath-----')
             path = LinearPath(dirn)
         else:
-            logger.debug("-----using RumPath------")
+            logger.debug('-----using RumPath------')
             # if rigid_units != None, but rotation_factors == None, then
             # raise an error.
-            if rotation_factors == None:
+            if rotation_factors is None:
                 raise RuntimeError(
-                    'RumPath cannot be created since rotation_factors == None')
+                    'RumPath cannot be created since rotation_factors == None'
+                )
             path = RumPath(x_start, dirn, rigid_units, rotation_factors)
-        # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
-
-        while (True):
 
-            logger.debug("-----------NEW ITERATION OF LINESEARCH----------")
-            logger.debug("Number of linesearch iterations: %d", num_iter)
-            logger.debug("a1 = %e", a1)
+        while True:
+            logger.debug('-----------NEW ITERATION OF LINESEARCH----------')
+            logger.debug('Number of linesearch iterations: %d', num_iter)
+            logger.debug('a1 = %e', a1)
 
             # CO replaced: func_a1 = self.func(x_start + a1 * self.dirn)
             func_a1 = self.func(x_start + path.step(a1))
             phi_a1 = func_a1
             # compute sufficient decrease (Armijo) condition
-            suff_dec = (phi_a1 <= self.func_start +
-                        self.c1 * a1 * self.phi_prime_start)
+            suff_dec = (
+                phi_a1 <= self.func_start + self.c1 * a1 * self.phi_prime_start
+            )
 
             # DEBUG
             # print("c1*a1*phi_prime_start = ", self.c1*a1*self.phi_prime_start,
             #       " | phi_a1 - phi_0 = ", phi_a1 - self.func_start)
-            logger.info("a1 = %.3f, suff_dec = %r", a1, suff_dec)
+            logger.info('a1 = %.3f, suff_dec = %r', a1, suff_dec)
             if a1 < self.a_min:
                 raise RuntimeError('a1 < a_min, giving up')
             if self.phi_prime_start > 0.0:
-                raise RuntimeError("self.phi_prime_start > 0.0")
+                raise RuntimeError('self.phi_prime_start > 0.0')
 
             # check sufficient decrease (Armijo condition)
             if suff_dec:
                 a_final = a1
                 phi_a_final = phi_a1
-                logger.debug("Linesearch returned a = %e, phi_a = %e",
-                             a_final, phi_a_final)
-                logger.debug("-----------LINESEARCH COMPLETE-----------")
+                logger.debug(
+                    'Linesearch returned a = %e, phi_a = %e',
+                    a_final,
+                    phi_a_final,
+                )
+                logger.debug('-----------LINESEARCH COMPLETE-----------')
                 return a_final, phi_a_final, num_iter == 0
 
             # we don't have sufficient decrease, so we need to compute a
             # new trial step-length
-            at = -  ((self.phi_prime_start * a1) /
-                     (2 * ((phi_a1 - self.func_start) / a1 - self.phi_prime_start)))
-            logger.debug("quadratic_min: initial at = %e", at)
+            at = -(
+                (self.phi_prime_start * a1)
+                / (2 * ((phi_a1 - self.func_start) / a1 - self.phi_prime_start))
+            )
+            logger.debug('quadratic_min: initial at = %e', at)
 
             # because a1 does not satisfy Armijo it follows that at must
             # lie between 0 and a1. In fact, more strongly,
-            #     at \leq (2 (1-c1))^{-1} a1, which is a back-tracking condition
-            # therefore, we should now only check that at has not become too small,
-            # in which case it is likely that nonlinearity has played a big role
-            # here, so we take an ultra-conservative backtracking step
+            # at \leq (2 (1-c1))^{-1} a1, which is a back-tracking condition
+            # therefore, we should now only check that at has not become
+            # too small, in which case it is likely that nonlinearity has
+            # played a big role here, so we take an ultra-conservative
+            # backtracking step
             a1 = max(at, a1 / 10.0)
             if a1 > at:
                 logger.debug(
-                    "at (%e) < a1/10: revert to backtracking a1/10", at)
+                    'at (%e) < a1/10: revert to backtracking a1/10', at
+                )
 
         # (end of while(True) line-search loop)
+
     # (end of run())
 
-    def handle_args(self, x_start, dirn, a_max, a_min, a1, func_start, func_old,
-                    func_prime_start, maxstep):
+    def handle_args(
+        self,
+        x_start,
+        dirn,
+        a_max,
+        a_min,
+        a1,
+        func_start,
+        func_old,
+        func_prime_start,
+        maxstep,
+    ):
         """Verify passed parameters and set appropriate attributes accordingly.
 
         A suitable value for the initial step-length guess will be either
@@ -369,66 +401,81 @@ class LineSearchArmijo:
             a_max = 2.0
 
         if a_max < self.tol:
-            logger.warning("a_max too small relative to tol. Reverting to "
-                           "default value a_max = 2.0 (twice the <ideal> step).")
-            a_max = 2.0    # THIS ASSUMES NEWTON/BFGS TYPE BEHAVIOUR!
+            logger.warning(
+                'a_max too small relative to tol. Reverting to '
+                'default value a_max = 2.0 (twice the <ideal> step).'
+            )
+            a_max = 2.0  # THIS ASSUMES NEWTON/BFGS TYPE BEHAVIOUR!
 
         if self.a_min is None:
             self.a_min = 1e-10
 
         if func_start is None:
-            logger.debug("Setting func_start")
+            logger.debug('Setting func_start')
             self.func_start = self.func(x_start)
 
         self.phi_prime_start = longsum(self.func_prime_start * self.dirn)
         if self.phi_prime_start >= 0:
             logger.error(
-                "Passed direction which is not downhill. Aborting...: %e",
-                self.phi_prime_start
+                'Passed direction which is not downhill. Aborting...: %e',
+                self.phi_prime_start,
             )
-            raise ValueError("Direction is not downhill.")
+            raise ValueError('Direction is not downhill.')
         elif math.isinf(self.phi_prime_start):
-            logger.error("Passed func_prime_start and dirn which are too big. "
-                         "Aborting...")
-            raise ValueError("func_prime_start and dirn are too big.")
+            logger.error(
+                'Passed func_prime_start and dirn which are too big. '
+                'Aborting...'
+            )
+            raise ValueError('func_prime_start and dirn are too big.')
 
         if a1 is None:
             if func_old is not None:
                 # Interpolating a quadratic to func and func_old - see NW
                 # equation 3.60
-                a1 = 2 * (self.func_start - self.func_old) / \
-                    self.phi_prime_start
-                logger.debug("Interpolated quadratic, obtained a1 = %e", a1)
+                a1 = (
+                    2 * (self.func_start - self.func_old) / self.phi_prime_start
+                )
+                logger.debug('Interpolated quadratic, obtained a1 = %e', a1)
         if a1 is None or a1 > a_max:
-            logger.debug("a1 greater than a_max. Reverting to default value "
-                         "a1 = 1.0")
+            logger.debug(
+                'a1 greater than a_max. Reverting to default value a1 = 1.0'
+            )
             a1 = 1.0
         if a1 is None or a1 < self.tol:
-            logger.debug("a1 is None or a1 < self.tol. Reverting to default value "
-                         "a1 = 1.0")
+            logger.debug(
+                'a1 is None or a1 < self.tol. Reverting to default value '
+                'a1 = 1.0'
+            )
             a1 = 1.0
         if a1 is None or a1 < self.a_min:
-            logger.debug("a1 is None or a1 < a_min. Reverting to default value "
-                         "a1 = 1.0")
+            logger.debug(
+                'a1 is None or a1 < a_min. Reverting to default value a1 = 1.0'
+            )
             a1 = 1.0
 
         if maxstep is None:
             maxstep = 0.2
-        logger.debug("maxstep = %e", maxstep)
+        logger.debug('maxstep = %e', maxstep)
 
         r = np.reshape(dirn, (-1, 3))
-        steplengths = ((a1 * r)**2).sum(1)**0.5
+        steplengths = ((a1 * r) ** 2).sum(1) ** 0.5
         maxsteplength = np.max(steplengths)
         if maxsteplength >= maxstep:
             a1 *= maxstep / maxsteplength
-            logger.debug("Rescaled a1 to fulfill maxstep criterion")
+            logger.debug('Rescaled a1 to fulfill maxstep criterion')
 
         self.a_start = a1
 
-        logger.debug("phi_start = %e, phi_prime_start = %e", self.func_start,
-                     self.phi_prime_start)
-        logger.debug("func_start = %s, self.func_old = %s", self.func_start,
-                     self.func_old)
-        logger.debug("a1 = %e, a_max = %e, a_min = %e", a1, a_max, self.a_min)
+        logger.debug(
+            'phi_start = %e, phi_prime_start = %e',
+            self.func_start,
+            self.phi_prime_start,
+        )
+        logger.debug(
+            'func_start = %s, self.func_old = %s',
+            self.func_start,
+            self.func_old,
+        )
+        logger.debug('a1 = %e, a_max = %e, a_min = %e', a1, a_max, self.a_min)
 
         return a1
diff -pruN 3.24.0-1/ase/utils/newrelease.py 3.26.0-1/ase/utils/newrelease.py
--- 3.24.0-1/ase/utils/newrelease.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/newrelease.py	2025-08-12 11:26:23.000000000 +0000
@@ -56,14 +56,16 @@ def get_version():
 
 
 def main():
-    p = argparse.ArgumentParser(description='Generate new release of ASE.',
-                                epilog='Run from the root directory of ASE.')
-    p.add_argument('version', nargs=1,
-                   help='version number for new release')
+    p = argparse.ArgumentParser(
+        description='Generate new release of ASE.',
+        epilog='Run from the root directory of ASE.',
+    )
+    p.add_argument('version', nargs=1, help='version number for new release')
     # p.add_argument('nextversion', nargs=1,
     #                help='development version after release')
-    p.add_argument('--clean', action='store_true',
-                   help='delete release branch and tag')
+    p.add_argument(
+        '--clean', action='store_true', help='delete release branch and tag'
+    )
     args = p.parse_args()
 
     assert versionfile.name == '__init__.py'
@@ -72,26 +74,33 @@ def main():
     try:
         current_version = get_version()
     except Exception as err:
-        p.error('Cannot get version: {}.  Are you in the root directory?'
-                .format(err))
+        p.error(
+            'Cannot get version: {}.  Are you in the root directory?'.format(
+                err
+            )
+        )
 
     print(f'Current version: {current_version}')
 
     version = args.version[0]
 
-    branchname = f'ase-{version}'
+    # branchname = f'ase-{version}'
     current_version = get_version()
 
     if args.clean:
         print(f'Cleaning {version}')
-        git('checkout master')
-        # git('tag -d {}'.format(version), error_ok=True)
-        git(f'branch -D {branchname}', error_ok=True)
-        # git('branch -D {}'.format('web-page'), error_ok=True)
+        # git('checkout master')
+        git(f'tag -d pre-{version}', error_ok=True)
+        # git(f'branch -D {branchname}', error_ok=True)
         return
 
     print(f'New release: {version}')
 
+    if shutil.which('scriv') is None:
+        p.error('No "scriv" command in PATH.  Is scriv installed?')
+
+    runcmd(f'scriv collect --add --title "Version {version}"')
+
     txt = git('status')
     branch = re.match(r'On branch (\S+)', txt).group(1)
 
@@ -113,7 +122,8 @@ def main():
     match_and_edit_version(
         versionfile,
         pattern='__version__ = ',
-        replacement=f"__version__ = '{version}'")
+        replacement=f"__version__ = '{version}'",
+    )
 
     releasenotes = ase_toplevel / 'doc/releasenotes.rst'
 
@@ -142,8 +152,9 @@ Git master branch
     date = strftime('%d %B %Y').lstrip('0')
     header = f'Version {version}'
     underline = '=' * len(header)
-    replacetxt = replacetxt.format(header=header, version=version,
-                                   underline=underline, date=date)
+    replacetxt = replacetxt.format(
+        header=header, version=version, underline=underline, date=date
+    )
 
     print(f'Editing {releasenotes}')
     with open(releasenotes) as fd:
@@ -184,23 +195,24 @@ News
     with open(installdoc) as fd:
         txt = fd.read()
 
-    txt, nsub = re.subn(r'ase-\d+\.\d+\.\d+',
-                        f'ase-{version}', txt)
+    txt, nsub = re.subn(r'ase-\d+\.\d+\.\d+', f'ase-{version}', txt)
     assert nsub > 0
-    txt, nsub = re.subn(r'git clone -b \d+\.\d+\.\d+',
-                        f'git clone -b {version}', txt)
+    txt, nsub = re.subn(
+        r'git clone -b \d+\.\d+\.\d+', f'git clone -b {version}', txt
+    )
     assert nsub == 1
 
     with open(installdoc, 'w') as fd:
         fd.write(txt)
 
     print(f'Creating new release from branch {branch!r}')
-    git(f'checkout -b {branchname}')
+    # git(f'checkout -b {branchname}')
 
     edited_paths = [versionfile, installdoc, frontpage, releasenotes]
 
     git('add {}'.format(' '.join(str(path) for path in edited_paths)))
     git(f'commit -m "ASE version {version}"')
+    git(f'tag pre-{version}')
     # git('tag -s {0} -m "ase-{0}"'.format(version))
 
     buildpath = Path('build')
@@ -229,11 +241,13 @@ News
     print('===============')
     print(f'git show {version}  # Inspect!')
     print('git checkout master')
-    print(f'git merge {branchname}')
-    print('twine upload '
-          'dist/ase-{v}.tar.gz '
-          'dist/ase-{v}-py3-none-any.whl '
-          'dist/ase-{v}.tar.gz.asc'.format(v=version))
+    # print(f'git merge {branchname}')
+    print(
+        'twine upload '
+        'dist/ase-{v}.tar.gz '
+        'dist/ase-{v}-py3-none-any.whl '
+        'dist/ase-{v}.tar.gz.asc'.format(v=version)
+    )
     print('git push --tags origin master  # Assuming your remote is "origin"')
 
 
diff -pruN 3.24.0-1/ase/utils/parsemath.py 3.26.0-1/ase/utils/parsemath.py
--- 3.24.0-1/ase/utils/parsemath.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/parsemath.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """A Module to safely parse/evaluate Mathematical Expressions"""
 import ast
 import math
diff -pruN 3.24.0-1/ase/utils/plotting.py 3.26.0-1/ase/utils/plotting.py
--- 3.24.0-1/ase/utils/plotting.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/plotting.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,19 +1,20 @@
-from typing import Optional
+from typing import Any, Optional
 
 import matplotlib.pyplot as plt
 from matplotlib.axes import Axes
-from matplotlib.figure import Figure
 
 
 class SimplePlottingAxes:
-    def __init__(self,
-                 ax: Optional[Axes] = None,
-                 show: bool = False,
-                 filename: str = None) -> None:
+    def __init__(
+        self,
+        ax: Optional[Axes] = None,
+        show: bool = False,
+        filename: str = None,
+    ) -> None:
         self.ax = ax
         self.show = show
         self.filename = filename
-        self.figure: Optional[Figure] = None
+        self.figure: Any = None  # Don't know about Figure/SubFigure etc
 
     def __enter__(self) -> Axes:
         if self.ax is None:
@@ -27,8 +28,9 @@ class SimplePlottingAxes:
         if exc_type is None:
             # If there was no exception, display/write the plot as appropriate
             if self.figure is None:
-                raise Exception("Something went wrong initializing matplotlib "
-                                "figure")
+                raise Exception(
+                    'Something went wrong initializing matplotlib figure'
+                )
             if self.show:
                 self.figure.show()
             if self.filename is not None:
diff -pruN 3.24.0-1/ase/utils/ptable.py 3.26.0-1/ase/utils/ptable.py
--- 3.24.0-1/ase/utils/ptable.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/ptable.py	2025-08-12 11:26:23.000000000 +0000
@@ -4,30 +4,27 @@ from ase import Atoms
 
 
 def ptable(spacing=2.5):
-    '''Generates the periodic table as an Atoms oobject to help with visualizing
-    rendering and color palette settings.'''
+    """Generates the periodic table as an Atoms oobject to help with visualizing
+    rendering and color palette settings."""
     # generates column, row positions for each element
     zmax = 118
+    z_values = np.arange(1, zmax + 1)  # z is atomic number not, position
+    positions = np.zeros((len(z_values), 3))
     x, y = 1, 1  # column, row , initial coordinates for Hydrogen
-    positions = np.zeros((zmax + 1, 3))
-    for z in range(1, zmax + 1):  # z is atomic number not, position
-        if z == 2:
+    for z in z_values:
+        if z == 2:  # right align He
             x += 16
-        if z == 5:
+        if z == 5 or z == 13:  # right align B and Al
             x += 10
-        if z == 13:
-            x += 10
-        if z == 57 or z == 89:
+        if z == 57 or z == 89:  # down shift lanthanides and actinides
             y += 3
-        if z == 72 or z == 104:
+        if z == 72 or z == 104:  # up/left shift last two transistion metal rows
             y -= 3
             x -= 14
-        positions[z] = (x, -y, 0)
+        positions[z - 1] = (x, -y, 0)
         x += 1
         if x > 18:
             x = 1
             y += 1
-    atoms = Atoms(np.arange(1, zmax + 1),
-                  positions[1:] * spacing)
-
+    atoms = Atoms(z_values, positions * spacing)
     return atoms
diff -pruN 3.24.0-1/ase/utils/sphinx.py 3.26.0-1/ase/utils/sphinx.py
--- 3.24.0-1/ase/utils/sphinx.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/sphinx.py	2025-08-12 11:26:23.000000000 +0000
@@ -24,7 +24,7 @@ def mol_role(role, rawtext, text, lineno
                 raise RuntimeError('Expected one or more digits after "_"')
             digits = m.group()
             n.append(nodes.subscript(text=digits))
-            text = text[1 + len(digits):]
+            text = text[1 + len(digits) :]
         else:
             t += text[0]
             text = text[1:]
@@ -32,40 +32,55 @@ def mol_role(role, rawtext, text, lineno
     return n, []
 
 
-def git_role_tmpl(urlroot,
-                  role,
-                  rawtext, text, lineno, inliner, options={}, content=[]):
+def git_role_tmpl(
+    urlroot, role, rawtext, text, lineno, inliner, options={}, content=[]
+):
+    env = inliner.document.settings.env
+    srcdir = Path(env.srcdir)
+    project_root = srcdir.parent
+    # The asserts below are commented out because this role template
+    # is also used by other projects, such as GPAW and Asap.
+    # assert srcdir.name == 'doc'
+    # assert project_root.name == 'ase'
+
     if text[-1] == '>':
         i = text.index('<')
-        name = text[:i - 1]
-        text = text[i + 1:-1]
+        name = text[: i - 1]
+        text = text[i + 1 : -1]
     else:
         name = text
         if name[0] == '~':
             name = name.split('/')[-1]
             text = text[1:]
         if '?' in name:
-            name = name[:name.index('?')]
+            name = name[: name.index('?')]
+
     # Check if the link is broken
     is_tag = text.startswith('..')  # Tags are like :git:`3.19.1 <../3.19.1>`
-    path = os.path.join('..', text)
-    do_exists = os.path.exists(path)
-    if not (is_tag or do_exists):
+    path = project_root / text
+
+    if not (is_tag or path.exists()):
         msg = f'Broken link: {rawtext}: Non-existing path: {path}'
         msg = inliner.reporter.error(msg, line=lineno)
         prb = inliner.problematic(rawtext, rawtext, msg)
         return [prb], [msg]
     ref = urlroot + text
     set_classes(options)
-    node = nodes.reference(rawtext, name, refuri=ref,
-                           **options)
+    node = nodes.reference(rawtext, name, refuri=ref, **options)
     return [node], []
 
 
 def git_role(role, rawtext, text, lineno, inliner, options={}, content=[]):
-    return git_role_tmpl('https://gitlab.com/ase/ase/blob/master/',
-                         role,
-                         rawtext, text, lineno, inliner, options, content)
+    return git_role_tmpl(
+        'https://gitlab.com/ase/ase/blob/master/',
+        role,
+        rawtext,
+        text,
+        lineno,
+        inliner,
+        options,
+        content,
+    )
 
 
 def setup(app):
@@ -93,8 +108,9 @@ def creates():
                 outnames = []
                 for line in lines:
                     if line.startswith('# creates:'):
-                        outnames.extend([file.rstrip(',')
-                                         for file in line.split()[2:]])
+                        outnames.extend(
+                            [file.rstrip(',') for file in line.split()[2:]]
+                        )
                     else:
                         break
                 if outnames:
@@ -103,6 +119,7 @@ def creates():
 
 def create_png_files(raise_exceptions=False):
     from ase.utils import workdir
+
     try:
         check_call(['povray', '-h'], stderr=DEVNULL)
     except (FileNotFoundError, CalledProcessError):
@@ -111,12 +128,18 @@ def create_png_files(raise_exceptions=Fa
         from ase.io import pov
         from ase.io.png import write_png
 
-        def write_pov(filename, atoms,
-                      povray_settings={}, isosurface_data=None,
-                      **generic_projection_settings):
-
-            write_png(Path(filename).with_suffix('.png'), atoms,
-                      **generic_projection_settings)
+        def write_pov(
+            filename,
+            atoms,
+            povray_settings={},
+            isosurface_data=None,
+            **generic_projection_settings,
+        ):
+            write_png(
+                Path(filename).with_suffix('.png'),
+                atoms,
+                **generic_projection_settings,
+            )
 
             class DummyRenderer:
                 def render(self):
@@ -144,6 +167,7 @@ def create_png_files(raise_exceptions=Fa
             print('running:', path)
             with workdir(dir):
                 import matplotlib.pyplot as plt
+
                 plt.figure()
                 try:
                     runpy.run_path(pyname)
@@ -173,6 +197,7 @@ def clean():
 def visual_inspection():
     """Manually inspect generated files."""
     import subprocess
+
     images = []
     text = []
     pdf = []
@@ -193,9 +218,14 @@ def visual_inspection():
 
 if __name__ == '__main__':
     import argparse
+
     parser = argparse.ArgumentParser(description='Process generated files.')
-    parser.add_argument('command', nargs='?', default='list',
-                        choices=['list', 'inspect', 'clean', 'run'])
+    parser.add_argument(
+        'command',
+        nargs='?',
+        default='list',
+        choices=['list', 'inspect', 'clean', 'run'],
+    )
     args = parser.parse_args()
     if args.command == 'clean':
         clean()
diff -pruN 3.24.0-1/ase/utils/structure_comparator.py 3.26.0-1/ase/utils/structure_comparator.py
--- 3.24.0-1/ase/utils/structure_comparator.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/structure_comparator.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,5 +1,6 @@
 """Determine symmetry equivalence of two structures.
 Based on the recipe from Comput. Phys. Commun. 183, 690-697 (2012)."""
+
 from collections import Counter
 from itertools import combinations, filterfalse, product
 
@@ -94,8 +95,15 @@ class SymmetryEquivalenceCheck:
 
     """
 
-    def __init__(self, angle_tol=1.0, ltol=0.05, stol=0.05, vol_tol=0.1,
-                 scale_volume=False, to_primitive=False):
+    def __init__(
+        self,
+        angle_tol=1.0,
+        ltol=0.05,
+        stol=0.05,
+        vol_tol=0.1,
+        scale_volume=False,
+        to_primitive=False,
+    ):
         self.angle_tol = angle_tol * np.pi / 180.0  # convert to radians
         self.scale_volume = scale_volume
         self.stol = stol
@@ -130,13 +138,13 @@ class SymmetryEquivalenceCheck:
         cell = atoms.get_cell().T
         total_rot_mat = np.eye(3)
         v1 = cell[:, 0]
-        l1 = np.sqrt(v1[0]**2 + v1[2]**2)
+        l1 = np.sqrt(v1[0] ** 2 + v1[2] ** 2)
         angle = np.abs(np.arcsin(v1[2] / l1))
-        if (v1[0] < 0.0 and v1[2] > 0.0):
+        if v1[0] < 0.0 and v1[2] > 0.0:
             angle = np.pi - angle
-        elif (v1[0] < 0.0 and v1[2] < 0.0):
+        elif v1[0] < 0.0 and v1[2] < 0.0:
             angle = np.pi + angle
-        elif (v1[0] > 0.0 and v1[2] < 0.0):
+        elif v1[0] > 0.0 and v1[2] < 0.0:
             angle = -angle
         ca = np.cos(angle)
         sa = np.sin(angle)
@@ -145,13 +153,13 @@ class SymmetryEquivalenceCheck:
         cell = rotmat.dot(cell)
 
         v1 = cell[:, 0]
-        l1 = np.sqrt(v1[0]**2 + v1[1]**2)
+        l1 = np.sqrt(v1[0] ** 2 + v1[1] ** 2)
         angle = np.abs(np.arcsin(v1[1] / l1))
-        if (v1[0] < 0.0 and v1[1] > 0.0):
+        if v1[0] < 0.0 and v1[1] > 0.0:
             angle = np.pi - angle
-        elif (v1[0] < 0.0 and v1[1] < 0.0):
+        elif v1[0] < 0.0 and v1[1] < 0.0:
             angle = np.pi + angle
-        elif (v1[0] > 0.0 and v1[1] < 0.0):
+        elif v1[0] > 0.0 and v1[1] < 0.0:
             angle = -angle
         ca = np.cos(angle)
         sa = np.sin(angle)
@@ -161,13 +169,13 @@ class SymmetryEquivalenceCheck:
 
         # Rotate around x axis such that the second vector is in the xy plane
         v2 = cell[:, 1]
-        l2 = np.sqrt(v2[1]**2 + v2[2]**2)
+        l2 = np.sqrt(v2[1] ** 2 + v2[2] ** 2)
         angle = np.abs(np.arcsin(v2[2] / l2))
-        if (v2[1] < 0.0 and v2[2] > 0.0):
+        if v2[1] < 0.0 and v2[2] > 0.0:
             angle = np.pi - angle
-        elif (v2[1] < 0.0 and v2[2] < 0.0):
+        elif v2[1] < 0.0 and v2[2] < 0.0:
             angle = np.pi + angle
-        elif (v2[1] > 0.0 and v2[2] < 0.0):
+        elif v2[1] > 0.0 and v2[2] < 0.0:
             angle = -angle
         ca = np.cos(angle)
         sa = np.sin(angle)
@@ -223,7 +231,7 @@ class SymmetryEquivalenceCheck:
         v1 = np.linalg.det(self.s1.get_cell())
 
         # Scale the cells
-        coordinate_scaling = (v1 / v2)**(1.0 / 3.0)
+        coordinate_scaling = (v1 / v2) ** (1.0 / 3.0)
         cell2 *= coordinate_scaling
         self.s2.set_cell(cell2, scale_atoms=True)
 
@@ -307,14 +315,14 @@ class SymmetryEquivalenceCheck:
                 # transposed version of the matrices to map atoms the
                 # other way
                 if transposed_matrices is None:
-                    transposed_matrices = np.transpose(matrices,
-                                                       axes=[0, 2, 1])
+                    transposed_matrices = np.transpose(matrices, axes=[0, 2, 1])
                 matrices = transposed_matrices
                 translations = self._get_least_frequent_positions(self.s1)
 
             # Calculate tolerance on positions
-            self.position_tolerance = \
-                self.stol * (vol / len(self.s2))**(1.0 / 3.0)
+            self.position_tolerance = self.stol * (vol / len(self.s2)) ** (
+                1.0 / 3.0
+            )
 
             if self._positions_match(matrices, translations):
                 return True
@@ -422,17 +430,22 @@ class SymmetryEquivalenceCheck:
         expanded_atoms = ref_atoms.copy()
 
         # Calculate normal vectors to the unit cell faces
-        normal_vectors = np.array([np.cross(cell[1, :], cell[2, :]),
-                                   np.cross(cell[0, :], cell[2, :]),
-                                   np.cross(cell[0, :], cell[1, :])])
+        normal_vectors = np.array(
+            [
+                np.cross(cell[1, :], cell[2, :]),
+                np.cross(cell[0, :], cell[2, :]),
+                np.cross(cell[0, :], cell[1, :]),
+            ]
+        )
         normalize(normal_vectors)
 
         # Get the distance to the unit cell faces from each atomic position
         pos2faces = np.abs(positions.dot(normal_vectors.T))
 
         # And the opposite faces
-        pos2oppofaces = np.abs(np.dot(positions - np.sum(cell, axis=0),
-                                      normal_vectors.T))
+        pos2oppofaces = np.abs(
+            np.dot(positions - np.sum(cell, axis=0), normal_vectors.T)
+        )
 
         for i, i2face in enumerate(pos2faces):
             # Append indices for positions close to the other faces
@@ -506,7 +519,7 @@ class SymmetryEquivalenceCheck:
 
         # Additional vector that is added to make sure that
         # there always is an atom at the origin
-        delta_vec = 1E-6 * cell_diag
+        delta_vec = 1e-6 * cell_diag
 
         # Store three reference vectors and their lengths
         ref_vec = self.s2.get_cell()
@@ -529,9 +542,9 @@ class SymmetryEquivalenceCheck:
         candidate_indices = []
         rtol = self.ltol / len(self.s1)
         for k in range(3):
-            correct_lengths_mask = np.isclose(lengths,
-                                              ref_vec_lengths[k],
-                                              rtol=rtol, atol=0)
+            correct_lengths_mask = np.isclose(
+                lengths, ref_vec_lengths[k], rtol=rtol, atol=0
+            )
             # The first vector is not interesting
             correct_lengths_mask[0] = False
 
@@ -556,9 +569,9 @@ class SymmetryEquivalenceCheck:
 
         # Calculate the dot product divided by the lengths:
         # cos(angle) = dot(vec1, vec2) / |vec1| |vec2|
-        cosa = np.inner(new_sc_pos[aci],
-                        new_sc_pos[aci]) / np.outer(lengths[aci],
-                                                    lengths[aci])
+        cosa = np.inner(new_sc_pos[aci], new_sc_pos[aci]) / np.outer(
+            lengths[aci], lengths[aci]
+        )
         # Make sure the inverse cosine will work
         cosa[cosa > 1] = 1
         cosa[cosa < -1] = -1
@@ -571,11 +584,16 @@ class SymmetryEquivalenceCheck:
         # that there are no duplicate candidates. product is the same as
         # nested for loops.
         refined_candidate_list = []
-        for p in filterfalse(self._equal_elements_in_array,
-                             product(*candidate_indices)):
-            a = np.array([angles[i2ang[p[0]], i2ang[p[1]]],
-                          angles[i2ang[p[0]], i2ang[p[2]]],
-                          angles[i2ang[p[1]], i2ang[p[2]]]])
+        for p in filterfalse(
+            self._equal_elements_in_array, product(*candidate_indices)
+        ):
+            a = np.array(
+                [
+                    angles[i2ang[p[0]], i2ang[p[1]]],
+                    angles[i2ang[p[0]], i2ang[p[2]]],
+                    angles[i2ang[p[1]], i2ang[p[2]]],
+                ]
+            )
 
             if np.allclose(a, ref_angles, atol=angle_tol, rtol=0):
                 refined_candidate_list.append(new_sc_pos[np.array(p)].T)
@@ -598,18 +616,16 @@ class SymmetryEquivalenceCheck:
         try:
             import spglib
         except ImportError:
-            raise SpgLibNotFoundError(
-                "SpgLib is required if to_primitive=True")
+            raise SpgLibNotFoundError('SpgLib is required if to_primitive=True')
         cell = (structure.get_cell()).tolist()
         pos = structure.get_scaled_positions().tolist()
         numbers = structure.get_atomic_numbers()
 
         cell, scaled_pos, numbers = spglib.standardize_cell(
-            (cell, pos, numbers), to_primitive=True)
+            (cell, pos, numbers), to_primitive=True
+        )
 
         atoms = Atoms(
-            scaled_positions=scaled_pos,
-            numbers=numbers,
-            cell=cell,
-            pbc=True)
+            scaled_positions=scaled_pos, numbers=numbers, cell=cell, pbc=True
+        )
         return atoms
diff -pruN 3.24.0-1/ase/utils/timing.py 3.26.0-1/ase/utils/timing.py
--- 3.24.0-1/ase/utils/timing.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/timing.py	2025-08-12 11:26:23.000000000 +0000
@@ -58,9 +58,10 @@ class Timer:
         names = tuple(self.running)
         running = self.running.pop()
         if name != running:
-            raise RuntimeError('Must stop timers by stack order.  '
-                               'Requested stopping of %s but topmost is %s'
-                               % (name, running))
+            raise RuntimeError(
+                'Must stop timers by stack order.  '
+                'Requested stopping of %s but topmost is %s' % (name, running)
+            )
         self.timers[names] += time.time()
         return names
 
@@ -132,8 +133,10 @@ class Timer:
             if level > self.print_levels:
                 continue
             name = (level - 1) * ' ' + names[-1] + ':'
-            out.write('%-*s%9.3f %9.3f %5.1f%% %s\n' %
-                      (n, name, tinclusive, t, p, bar))
+            out.write(
+                '%-*s%9.3f %9.3f %5.1f%% %s\n'
+                % (n, name, tinclusive, t, p, bar)
+            )
         out.write(line)
         out.write('%-*s%9.3f %5.1f%%\n\n' % (n + 10, 'Total:', tot, 100.0))
 
@@ -160,13 +163,14 @@ class timer:
             def add(self, x, y):
                 return x + y
 
-        """
+    """
 
     def __init__(self, name):
         self.name = name
 
     def __call__(self, method):
         if inspect.isgeneratorfunction(method):
+
             @functools.wraps(method)
             def new_method(slf, *args, **kwargs):
                 gen = method(slf, *args, **kwargs)
@@ -180,6 +184,7 @@ class timer:
                         slf.timer.stop()
                     yield x
         else:
+
             @functools.wraps(method)
             def new_method(slf, *args, **kwargs):
                 slf.timer.start(self.name)
@@ -189,4 +194,5 @@ class timer:
                 except IndexError:
                     pass
                 return x
+
         return new_method
diff -pruN 3.24.0-1/ase/utils/xrdebye.py 3.26.0-1/ase/utils/xrdebye.py
--- 3.24.0-1/ase/utils/xrdebye.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/utils/xrdebye.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 # flake8: noqa
 """Definition of the XrDebye class.
 
diff -pruN 3.24.0-1/ase/vibrations/albrecht.py 3.26.0-1/ase/vibrations/albrecht.py
--- 3.24.0-1/ase/vibrations/albrecht.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/vibrations/albrecht.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import sys
 from itertools import combinations_with_replacement
 
diff -pruN 3.24.0-1/ase/vibrations/data.py 3.26.0-1/ase/vibrations/data.py
--- 3.24.0-1/ase/vibrations/data.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/vibrations/data.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Storage and analysis for vibrational data"""
 
 import collections
@@ -127,7 +129,8 @@ class VibrationsData:
                 range(
                     len(atoms))),
             const_indices).astype(int)
-        return indices.tolist()
+        # TODO: use numpy.typing to resolve this error.
+        return indices.tolist()  # type: ignore[return-value]
 
     @staticmethod
     def indices_from_mask(mask: Union[Sequence[bool], np.ndarray]
@@ -153,7 +156,8 @@ class VibrationsData:
             indices of True elements
 
         """
-        return np.where(mask)[0].tolist()
+        # TODO: use numpy.typing to resolve this error.
+        return np.where(mask)[0].tolist()  # type: ignore[return-value]
 
     @staticmethod
     def _check_dimensions(atoms: Atoms,
@@ -399,11 +403,6 @@ class VibrationsData:
     def get_zero_point_energy(self) -> float:
         """Diagonalise the Hessian and sum hw/2 to obtain zero-point energy
 
-        Args:
-            energies:
-                Pre-computed energy eigenvalues. Use if available to avoid
-                re-calculating these from the Hessian.
-
         Returns:
             zero-point energy in eV
         """
diff -pruN 3.24.0-1/ase/vibrations/franck_condon.py 3.26.0-1/ase/vibrations/franck_condon.py
--- 3.24.0-1/ase/vibrations/franck_condon.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/vibrations/franck_condon.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from functools import reduce
 from itertools import chain, combinations
 from math import factorial
diff -pruN 3.24.0-1/ase/vibrations/infrared.py 3.26.0-1/ase/vibrations/infrared.py
--- 3.24.0-1/ase/vibrations/infrared.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/vibrations/infrared.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Infrared intensities"""
 
 from math import sqrt
diff -pruN 3.24.0-1/ase/vibrations/placzek.py 3.26.0-1/ase/vibrations/placzek.py
--- 3.24.0-1/ase/vibrations/placzek.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/vibrations/placzek.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 import ase.units as u
diff -pruN 3.24.0-1/ase/vibrations/raman.py 3.26.0-1/ase/vibrations/raman.py
--- 3.24.0-1/ase/vibrations/raman.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/vibrations/raman.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import numpy as np
 
 import ase.units as u
diff -pruN 3.24.0-1/ase/vibrations/resonant_raman.py 3.26.0-1/ase/vibrations/resonant_raman.py
--- 3.24.0-1/ase/vibrations/resonant_raman.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/vibrations/resonant_raman.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Resonant Raman intensities"""
 
 import sys
diff -pruN 3.24.0-1/ase/vibrations/vibrations.py 3.26.0-1/ase/vibrations/vibrations.py
--- 3.24.0-1/ase/vibrations/vibrations.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/vibrations/vibrations.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """A class for computing vibrational modes"""
 
 import sys
diff -pruN 3.24.0-1/ase/visualize/__init__.py 3.26.0-1/ase/visualize/__init__.py
--- 3.24.0-1/ase/visualize/__init__.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/visualize/__init__.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import ase.parallel as parallel
 
 
diff -pruN 3.24.0-1/ase/visualize/mlab.py 3.26.0-1/ase/visualize/mlab.py
--- 3.24.0-1/ase/visualize/mlab.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/visualize/mlab.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 import optparse
 
 import numpy as np
diff -pruN 3.24.0-1/ase/visualize/ngl.py 3.26.0-1/ase/visualize/ngl.py
--- 3.24.0-1/ase/visualize/ngl.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/visualize/ngl.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase import Atoms
 
 
diff -pruN 3.24.0-1/ase/visualize/paraview_script.py 3.26.0-1/ase/visualize/paraview_script.py
--- 3.24.0-1/ase/visualize/paraview_script.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/visualize/paraview_script.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 def main():
     import paraview.simple as para
     version_major = para.servermanager.vtkSMProxyManager.GetVersionMajor()
diff -pruN 3.24.0-1/ase/visualize/plot.py 3.26.0-1/ase/visualize/plot.py
--- 3.24.0-1/ase/visualize/plot.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/visualize/plot.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.io.utils import PlottingVariables, make_patch_list
 
 
diff -pruN 3.24.0-1/ase/visualize/sage.py 3.26.0-1/ase/visualize/sage.py
--- 3.24.0-1/ase/visualize/sage.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/visualize/sage.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 from ase.data import covalent_radii
 from ase.data.colors import jmol_colors
 
diff -pruN 3.24.0-1/ase/visualize/viewers.py 3.26.0-1/ase/visualize/viewers.py
--- 3.24.0-1/ase/visualize/viewers.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/visualize/viewers.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """
 Module for managing viewers
 
diff -pruN 3.24.0-1/ase/visualize/x3d.py 3.26.0-1/ase/visualize/x3d.py
--- 3.24.0-1/ase/visualize/x3d.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/ase/visualize/x3d.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,3 +1,5 @@
+# fmt: off
+
 """Inline viewer for jupyter notebook using X3D."""
 import warnings
 
diff -pruN 3.24.0-1/bin/ase 3.26.0-1/bin/ase
--- 3.24.0-1/bin/ase	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/bin/ase	1970-01-01 00:00:00.000000000 +0000
@@ -1,4 +0,0 @@
-#!/usr/bin/env python3
-from ase.cli.main import main
-
-main()
diff -pruN 3.24.0-1/debian/changelog 3.26.0-1/debian/changelog
--- 3.24.0-1/debian/changelog	2025-03-06 03:23:13.000000000 +0000
+++ 3.26.0-1/debian/changelog	2025-10-10 07:18:56.000000000 +0000
@@ -1,3 +1,11 @@
+python-ase (3.26.0-1) unstable; urgency=medium
+
+  * New upstream version 3.26.0 (Closes: #1116437)
+  * Adapt to the switch from python3-sphinx-rtd-theme to
+    python3-sphinx-book-theme.
+
+ -- Andrius Merkys <merkys@debian.org>  Fri, 10 Oct 2025 03:18:56 -0400
+
 python-ase (3.24.0-1) unstable; urgency=medium
 
   [ Alexandre Detiste ]
diff -pruN 3.24.0-1/debian/control 3.26.0-1/debian/control
--- 3.24.0-1/debian/control	2025-03-06 03:23:13.000000000 +0000
+++ 3.26.0-1/debian/control	2025-10-09 10:29:55.000000000 +0000
@@ -22,7 +22,7 @@ Build-Depends: debhelper-compat (= 13),
                python3-scipy,
                python3-setuptools,
                python3-sphinx,
-               python3-sphinx-rtd-theme,
+               python3-sphinx-book-theme,
                python3-tk <!nocheck>,
                texlive-latex-base <!nodoc>,
                python3-doc <!nodoc>
diff -pruN 3.24.0-1/debian/patches/doc_intersphinx.patch 3.26.0-1/debian/patches/doc_intersphinx.patch
--- 3.24.0-1/debian/patches/doc_intersphinx.patch	2025-03-06 03:23:13.000000000 +0000
+++ 3.26.0-1/debian/patches/doc_intersphinx.patch	2025-10-09 09:23:51.000000000 +0000
@@ -1,13 +1,11 @@
-Index: python-ase/doc/conf.py
-===================================================================
---- python-ase.orig/doc/conf.py	2025-03-04 18:41:44.388496282 +0100
-+++ python-ase/doc/conf.py	2025-03-04 19:46:53.456253506 +0100
-@@ -48,7 +48,7 @@
-     ('index', 'ASE.tex', 'ASE', 'ASE-developers', 'howto', not True)]
+--- a/doc/conf.py
++++ b/doc/conf.py
+@@ -61,7 +61,7 @@
  
- intersphinx_mapping = {'gpaw': ('https://gpaw.readthedocs.io', None),
--                       'python': ('https://docs.python.org/3.10', None)}
-+                       'python': ('https://docs.python.org/3.10', ('/usr/share/doc/python3/html/objects.inv', None))}
+ intersphinx_mapping = {
+     'gpaw': ('https://gpaw.readthedocs.io', None),
+-    'python': ('https://docs.python.org/3.10', None),
++    'python': ('https://docs.python.org/3.10', ('/usr/share/doc/python3/html/objects.inv', None)),
+ }
  
  # Avoid GUI windows during doctest:
- doctest_global_setup = """
diff -pruN 3.24.0-1/debian/patches/re_maxsplit.patch 3.26.0-1/debian/patches/re_maxsplit.patch
--- 3.24.0-1/debian/patches/re_maxsplit.patch	2025-03-06 03:23:13.000000000 +0000
+++ 3.26.0-1/debian/patches/re_maxsplit.patch	1970-01-01 00:00:00.000000000 +0000
@@ -1,20 +0,0 @@
-Index: python-ase/ase/io/castep/__init__.py
-===================================================================
---- python-ase.orig/ase/io/castep/__init__.py	2025-03-04 18:41:44.356496046 +0100
-+++ python-ase/ase/io/castep/__init__.py	2025-03-04 19:11:08.861484534 +0100
-@@ -385,13 +385,13 @@
-     for i, l in enumerate(filelines):
- 
-         # Strip all comments, aka anything after a hash
--        L = re.split(r'[#!;]', l, 1)[0].strip()
-+        L = re.split(r'[#!;]', l, maxsplit=1)[0].strip()
- 
-         if L == '':
-             # Empty line... skip
-             continue
- 
--        lsplit = re.split(r'\s*[:=]*\s+', L, 1)
-+        lsplit = re.split(r'\s*[:=]*\s+', L, maxsplit=1)
- 
-         if read_block:
-             if lsplit[0].lower() == '%endblock':
diff -pruN 3.24.0-1/debian/patches/series 3.26.0-1/debian/patches/series
--- 3.24.0-1/debian/patches/series	2025-03-06 03:23:13.000000000 +0000
+++ 3.26.0-1/debian/patches/series	2025-10-09 09:22:30.000000000 +0000
@@ -1,3 +1,2 @@
 skip-failing-tests.patch
-re_maxsplit.patch
 doc_intersphinx.patch
diff -pruN 3.24.0-1/debian/patches/skip-failing-tests.patch 3.26.0-1/debian/patches/skip-failing-tests.patch
--- 3.24.0-1/debian/patches/skip-failing-tests.patch	2025-03-06 03:23:13.000000000 +0000
+++ 3.26.0-1/debian/patches/skip-failing-tests.patch	2025-10-09 09:21:46.000000000 +0000
@@ -1,8 +1,6 @@
-Index: python-ase/ase/test/calculator/socketio/test_ipi_protocol_bfgs.py
-===================================================================
---- python-ase.orig/ase/test/calculator/socketio/test_ipi_protocol_bfgs.py	2024-08-01 10:48:19.864875159 +0200
-+++ python-ase/ase/test/calculator/socketio/test_ipi_protocol_bfgs.py	2024-08-01 10:48:19.856874923 +0200
-@@ -108,6 +108,7 @@
+--- a/ase/test/calculator/socketio/test_ipi_protocol_bfgs.py
++++ b/ase/test/calculator/socketio/test_ipi_protocol_bfgs.py
+@@ -109,6 +109,7 @@
      'inet',
      pytest.param('unix', marks=unix_only),
  ])
diff -pruN 3.24.0-1/debian/rules 3.26.0-1/debian/rules
--- 3.24.0-1/debian/rules	2025-03-06 03:23:13.000000000 +0000
+++ 3.26.0-1/debian/rules	2025-10-10 07:18:28.000000000 +0000
@@ -9,8 +9,7 @@ export PYBUILD_NAME=ase
 
 override_dh_auto_test:
 	PYBUILD_SYSTEM=custom \
-		PYBUILD_TEST_ARGS="cd ase/test; {interpreter} -m ase test --pytest -k 'not test_versionnumber'" \
-		PATH=$(CURDIR)/bin:$$PATH \
+		PYBUILD_TEST_ARGS="cd ase/test; PATH={build_dir}/../scripts:$$PATH {interpreter} -m ase test --pytest -k 'not test_versionnumber'" \
 		LC_ALL=C.UTF-8 \
 		TERM=linux \
 		dh_auto_test
@@ -59,6 +58,6 @@ override_dh_python3:
 override_dh_installman:
 	PYTHONPATH=$(CURDIR) \
 		help2man --version-string $(DEB_VERSION_UPSTREAM) -N -n "ASE command line tool" \
-		bin/ase -o $(CURDIR)/debian/ase.1
+		$(CURDIR)/debian/ase/usr/bin/ase -o $(CURDIR)/debian/ase.1
 	sed -i '/^{/s/,/, /g' $(CURDIR)/debian/ase.1
 	dh_installman
diff -pruN 3.24.0-1/doc/ase/atoms.py 3.26.0-1/doc/ase/atoms.py
--- 3.24.0-1/doc/ase/atoms.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/atoms.py	2025-08-12 11:26:23.000000000 +0000
@@ -5,15 +5,14 @@ from ase.io import write
 
 d = 2.9
 L = 10.0
-wire = Atoms('Au',
-             positions=[(0, L / 2, L / 2)],
-             cell=(d, L, L),
-             pbc=(1, 0, 0))
+wire = Atoms('Au', positions=[(0, L / 2, L / 2)], cell=(d, L, L), pbc=(1, 0, 0))
 wire *= (6, 1, 1)
 wire.positions[:, 0] -= 2 * d
 wire.cell[0, 0] = d
 # view(wire, block=1)
-write('Au-wire.pov', wire,
-      rotation='12x,6y',
-      povray_settings=dict(
-          transparent=False)).render()
+write(
+    'Au-wire.pov',
+    wire,
+    rotation='12x,6y',
+    povray_settings=dict(transparent=False),
+).render()
diff -pruN 3.24.0-1/doc/ase/build/general_surface.py 3.26.0-1/doc/ase/build/general_surface.py
--- 3.24.0-1/doc/ase/build/general_surface.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/build/general_surface.py	2025-08-12 11:26:23.000000000 +0000
@@ -16,13 +16,12 @@ s2 = surface(Mobulk, (3, 2, 1), 9)
 s2.center(vacuum=10, axis=2)
 # Pt3Rh example:
 a = 4.0
-Pt3Rh = Atoms('Pt3Rh',
-              scaled_positions=[(0, 0, 0),
-                                (0.5, 0.5, 0),
-                                (0.5, 0, 0.5),
-                                (0, 0.5, 0.5)],
-              cell=[a, a, a],
-              pbc=True)
+Pt3Rh = Atoms(
+    'Pt3Rh',
+    scaled_positions=[(0, 0, 0), (0.5, 0.5, 0), (0.5, 0, 0.5), (0, 0.5, 0.5)],
+    cell=[a, a, a],
+    pbc=True,
+)
 s3 = surface(Pt3Rh, (2, 1, 1), 9)
 s3.center(vacuum=10, axis=2)
 
@@ -32,20 +31,22 @@ s4.center(vacuum=10, axis=2)
 # Done
 
 for atoms, name in [(s1, 's1'), (s2, 's2'), (s3, 's3'), (s4, 's4')]:
-    write(name + '.pov', atoms,
-          rotation='-90x',
-          povray_settings=dict(
-              transparent=False)).render()
+    write(
+        name + '.pov',
+        atoms,
+        rotation='-90x',
+        povray_settings=dict(transparent=False),
+    ).render()
 
 
 dir = os.environ.get('PDF_FILE_DIR')
 if dir:
-    shutil.copyfile(Path(dir) / 'general_surface.pdf',
-                    'general_surface.pdf')
+    shutil.copyfile(Path(dir) / 'general_surface.pdf', 'general_surface.pdf')
 else:
     for i in range(2):
         error = os.system(
-            'pdflatex -interaction=nonstopmode general_surface > /dev/null')
+            'pdflatex -interaction=nonstopmode general_surface > /dev/null'
+        )
         if error:
             with open('general_surface.pdf', 'w') as fd:
                 fd.write('pdflatex not found\n')
diff -pruN 3.24.0-1/doc/ase/build/structure.py 3.26.0-1/doc/ase/build/structure.py
--- 3.24.0-1/doc/ase/build/structure.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/build/structure.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,9 +3,12 @@ from ase.build import bulk, graphene_nan
 from ase.io import write
 
 for i, a in enumerate(
-    [bulk('Cu', 'fcc', a=3.6),
-     bulk('Cu', 'fcc', a=3.6, orthorhombic=True),
-     bulk('Cu', 'fcc', a=3.6, cubic=True)]):
+    [
+        bulk('Cu', 'fcc', a=3.6),
+        bulk('Cu', 'fcc', a=3.6, orthorhombic=True),
+        bulk('Cu', 'fcc', a=3.6, cubic=True),
+    ]
+):
     write('a%d.pov' % (i + 1), a).render()
 
 cnt1 = nanotube(6, 0, length=4, vacuum=2.5)
@@ -17,10 +20,17 @@ for i, a in enumerate([cnt1, cnt2]):
     write('cnt%d.pov' % (i + 1), a).render()
 
 gnr1 = graphene_nanoribbon(3, 4, type='armchair', saturated=True, vacuum=2.5)
-gnr2 = graphene_nanoribbon(2, 6, type='zigzag', saturated=True,
-                           C_H=1.1, C_C=1.4, vacuum=3.0,
-                           magnetic=True, initial_mag=1.12)
+gnr2 = graphene_nanoribbon(
+    2,
+    6,
+    type='zigzag',
+    saturated=True,
+    C_H=1.1,
+    C_C=1.4,
+    vacuum=3.0,
+    magnetic=True,
+    initial_mag=1.12,
+)
 
 for i, a in enumerate([gnr1, gnr2]):
-    write('gnr%d.pov' % (i + 1), a,
-          rotation='90x').render()
+    write('gnr%d.pov' % (i + 1), a, rotation='90x').render()
diff -pruN 3.24.0-1/doc/ase/build/surface.py 3.26.0-1/doc/ase/build/surface.py
--- 3.24.0-1/doc/ase/build/surface.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/build/surface.py	2025-08-12 11:26:23.000000000 +0000
@@ -10,9 +10,21 @@ from ase import Atoms
 from ase.build import fcc111, root_surface
 from ase.io import write
 
-surfaces = ['fcc100', 'fcc110', 'bcc100', 'hcp10m10', 'diamond100',
-            'fcc111', 'bcc110', 'bcc111', 'hcp0001', 'diamond111', 'fcc211',
-            'mx2', 'graphene']
+surfaces = [
+    'fcc100',
+    'fcc110',
+    'bcc100',
+    'hcp10m10',
+    'diamond100',
+    'fcc111',
+    'bcc110',
+    'bcc111',
+    'hcp0001',
+    'diamond111',
+    'fcc211',
+    'mx2',
+    'graphene',
+]
 
 symbols = {
     'fcc': 'Cu',
@@ -20,22 +32,28 @@ symbols = {
     'hcp': 'Ru',
     'dia': 'C',
     'mx2': 'MoS2',
-    'gra': 'C2'}
+    'gra': 'C2',
+}
 radii = {
     'fcc': 1.1,
     'bcc': 1.06,
     'hcp': 1.08,
     'dia': 0.5,
     'mx2': 1.0,
-    'gra': 1.0}
-adsorbates = {'ontop': 'H', 'hollow': 'O', 'fcc': 'N', 'hcp': 'C',
-              'bridge': 'F'}
+    'gra': 1.0,
+}
+adsorbates = {
+    'ontop': 'H',
+    'hollow': 'O',
+    'fcc': 'N',
+    'hcp': 'C',
+    'bridge': 'F',
+}
 
 
 def save(name, slab):
     print('save %s' % name)
-    write(name + '.png', slab, radii=radii[name[:3]],
-          scale=10)
+    write(name + '.png', slab, radii=radii[name[:3]], scale=10)
 
 
 for name in surfaces:
diff -pruN 3.24.0-1/doc/ase/calculators/NaCl.py 3.26.0-1/doc/ase/calculators/NaCl.py
--- 3.24.0-1/doc/ase/calculators/NaCl.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/NaCl.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,13 +3,12 @@ from ase.calculators.vasp import Vasp
 
 a = [6.5, 6.5, 7.7]
 d = 2.3608
-NaCl = Atoms([Atom('Na', [0, 0, 0], magmom=1.928),
-              Atom('Cl', [0, 0, d], magmom=0.75)],
-             cell=a)
+NaCl = Atoms(
+    [Atom('Na', [0, 0, 0], magmom=1.928), Atom('Cl', [0, 0, d], magmom=0.75)],
+    cell=a,
+)
 
-calc = Vasp(prec='Accurate',
-            xc='PBE',
-            lreal=False)
+calc = Vasp(prec='Accurate', xc='PBE', lreal=False)
 NaCl.calc = calc
 
 print(NaCl.get_magnetic_moment())
diff -pruN 3.24.0-1/doc/ase/calculators/ase_castep_demo.py 3.26.0-1/doc/ase/calculators/ase_castep_demo.py
--- 3.24.0-1/doc/ase/calculators/ase_castep_demo.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/ase_castep_demo.py	2025-08-12 11:26:23.000000000 +0000
@@ -53,7 +53,7 @@ mol.calc = calc
 if calc.dryrun_ok():
     print(f'{mol.calc._label} : {mol.get_potential_energy()} ')
 else:
-    print("Found error in input")
+    print('Found error in input')
     print(calc._error)
 
 
diff -pruN 3.24.0-1/doc/ase/calculators/calculators.rst 3.26.0-1/doc/ase/calculators/calculators.rst
--- 3.24.0-1/doc/ase/calculators/calculators.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/calculators.rst	2025-08-12 11:26:23.000000000 +0000
@@ -40,7 +40,7 @@ Supported calculators
 
 The calculators can be divided in four groups:
 
-1) Abacus_, ALIGNN_, AMS_, Asap_, BigDFT_, CHGNet_, DeePMD-kit_, DFTD3_, DFTD4_, DFTK_, FLEUR_, GPAW_, Hotbit_, M3GNet_, MACE_, TBLite_, and XTB_
+1) Abacus_, ALIGNN_, AMS_, Asap_, BigDFT_, CHGNet_, DeePMD-kit_, DFTD3_, DFTD4_, DFTK_, EquiFormerV2_, FLEUR_, GPAW_, Hotbit_, M3GNet_, MACE_, OrbModels_, SevenNet_, TBLite_, and XTB_
    have their own native or external ASE interfaces.
 
 2) ABINIT, AMBER, CP2K, CASTEP, deMon2k, DFTB+, ELK, EXCITING, FHI-aims, GAUSSIAN,
@@ -49,7 +49,7 @@ The calculators can be divided in four g
    FORTRAN/C/C++ codes are not part of ASE.
 
 3) Pure python implementations included in the ASE package: EMT, EAM,
-   Lennard-Jones, Morse and HarmonicCalculator.
+   Lennard-Jones, Morse, Tersoff, and HarmonicCalculator.
 
 4) Calculators that wrap others, included in the ASE package:
 
@@ -82,11 +82,14 @@ DeePMD-kit_
 DFTD3_                                    London-dispersion correction
 DFTD4_                                    Charge-dependent London-dispersion correction
 DFTK_                                     Plane-wave code for DFT and related models
+EquiFormerV2_                             Equivariant graph-based denoising transformer universal potential
 FLEUR_                                    Full Potential LAPW code
 GPAW_                                     Real-space/plane-wave/LCAO PAW code
 Hotbit_                                   DFT based tight binding
 M3GNet_                                   Materials 3-body Graph Network universal potential
 MACE_                                     Many-body potential using higher-order equivariant message passing
+OrbModels_                                Fast, scalable, universal GNN potentials with diffusion pretraining
+SevenNet_                                 Scalable EquiVariance Enabled Neural Network interatomic potential
 TBLite_                                   Light-weight tight-binding framework
 XTB_                                      Semiemprical extended tight-binding program package
 :mod:`~ase.calculators.abinit`            Plane-wave pseudopotential code
@@ -98,7 +101,7 @@ XTB_
 :mod:`~ase.calculators.dftb`              DFT based tight binding
 :mod:`~ase.calculators.dmol`              Atomic orbital DFT code
 :mod:`~ase.calculators.eam`               Embedded Atom Method
-elk                                       Full Potential LAPW code
+:mod:`~ase.calculators.elk`               Full Potential LAPW code
 :mod:`~ase.calculators.espresso`          Plane-wave pseudopotential code
 :mod:`~ase.calculators.exciting`          Full Potential LAPW code
 :mod:`~ase.calculators.aims`              Numeric atomic orbital, full potential code
@@ -121,6 +124,7 @@ elk
 :mod:`~ase.calculators.qchem`             Gaussian based electronic structure code
 :mod:`~ase.calculators.siesta`            LCAO pseudopotential code
 :mod:`~ase.calculators.turbomole`         Fast atom orbital code
+:mod:`~ase.calculators.tersoff`           Tersoff bond-order potential
 :mod:`~ase.calculators.vasp`              Plane-wave PAW code
 :mod:`~ase.calculators.emt`               Effective Medium Theory calculator
 lj                                        Lennard-Jones potential
@@ -162,12 +166,17 @@ where ``abc`` is the module name and ``A
 .. _DeePMD-kit: https://github.com/deepmodeling/deepmd-kit
 .. _DFTD4: https://github.com/dftd4/dftd4/tree/main/python
 .. _DFTD3: https://dftd3.readthedocs.io/en/latest/api/python.html#module-dftd3.ase
+.. _EquiFormerV2: https://github.com/FAIR-Chem/fairchem#quick-start
 .. _FLEUR: https://github.com/JuDFTteam/ase-fleur
 .. _M3GNet: https://matgl.ai/matgl.ext.html#class-matglextasem3gnetcalculatorpotential-potential-state_attr-torchtensor--none--none-stress_weight-float--10-kwargs
 .. _MACE: https://mace-docs.readthedocs.io/en/latest/guide/ase.html
+.. _OrbModels: https://github.com/orbital-materials/orb-models/tree/main#usage-with-ase-calculator
+.. _SevenNet: https://github.com/MDIL-SNU/SevenNet#ase-calculator
 .. _TBLite: https://tblite.readthedocs.io/en/latest/users/ase.html
 .. _XTB: https://xtb-python.readthedocs.io/en/latest/ase-calculator.html
 
+.. _calculator-configuration:
+
 Calculator configuration
 ========================
 
@@ -224,6 +233,7 @@ to set the ``ASE_CONFIG_PATH`` to an emp
 
 
 .. toctree::
+   :maxdepth: 1
 
    eam
    emt
@@ -236,6 +246,7 @@ to set the ``ASE_CONFIG_PATH`` to an emp
    demonnano
    dftb
    dmol
+   elk
    espresso
    exciting
    FHI-aims
@@ -260,6 +271,7 @@ to set the ``ASE_CONFIG_PATH`` to an emp
    qchem
    siesta
    turbomole
+   tersoff
    vasp
    qmmm
    checkpointing
diff -pruN 3.24.0-1/doc/ase/calculators/demonnano_ex1_relax.py 3.26.0-1/doc/ase/calculators/demonnano_ex1_relax.py
--- 3.24.0-1/doc/ase/calculators/demonnano_ex1_relax.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/demonnano_ex1_relax.py	2025-08-12 11:26:23.000000000 +0000
@@ -7,14 +7,11 @@ from ase.optimize import BFGS
 
 d = 0.9775
 t = np.pi / 180 * 110.51
-mol = Atoms('H2O',
-            positions=[(d, 0, 0),
-                       (d * np.cos(t), d * np.sin(t), 0),
-                       (0, 0, 0)])
+mol = Atoms(
+    'H2O', positions=[(d, 0, 0), (d * np.cos(t), d * np.sin(t), 0), (0, 0, 0)]
+)
 
-input_arguments = {'DFTB': 'SCC',
-                   'CHARGE': '0.0',
-                   'PARAM': 'PTYPE=BIO'}
+input_arguments = {'DFTB': 'SCC', 'CHARGE': '0.0', 'PARAM': 'PTYPE=BIO'}
 
 calc = DemonNano(label='rundir/', input_arguments=input_arguments)
 mol.calc = calc
diff -pruN 3.24.0-1/doc/ase/calculators/dftb.rst 3.26.0-1/doc/ase/calculators/dftb.rst
--- 3.24.0-1/doc/ase/calculators/dftb.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/dftb.rst	2025-08-12 11:26:23.000000000 +0000
@@ -98,7 +98,7 @@ arguments can be used:
         * ``[(k11,k12,k13),(k21,k22,k23),...]``: Explicit (Nkpts x 3) array of k-points
           in units of the reciprocal lattice vectors (each with equal weight)
 
-.. _path: https://wiki.fysik.dtu.dk/ase/ase/dft/kpoints.html#ase.dft.kpoints.bandpath
+.. _path: https://ase-lib.org/ase/dft/kpoints.html#ase.dft.kpoints.bandpath
 
 
 Examples
diff -pruN 3.24.0-1/doc/ase/calculators/dftb_ex1_relax.py 3.26.0-1/doc/ase/calculators/dftb_ex1_relax.py
--- 3.24.0-1/doc/ase/calculators/dftb_ex1_relax.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/dftb_ex1_relax.py	2025-08-12 11:26:23.000000000 +0000
@@ -4,11 +4,12 @@ from ase.io import write
 from ase.optimize import QuasiNewton
 
 atoms = molecule('H2O')
-calc = Dftb(label='h2o',
-            Hamiltonian_MaxAngularMomentum_='',
-            Hamiltonian_MaxAngularMomentum_O='p',
-            Hamiltonian_MaxAngularMomentum_H='s',
-            )
+calc = Dftb(
+    label='h2o',
+    Hamiltonian_MaxAngularMomentum_='',
+    Hamiltonian_MaxAngularMomentum_O='p',
+    Hamiltonian_MaxAngularMomentum_H='s',
+)
 atoms.calc = calc
 
 dyn = QuasiNewton(atoms, trajectory='test.traj')
diff -pruN 3.24.0-1/doc/ase/calculators/dftb_ex2_relaxbyDFTB.py 3.26.0-1/doc/ase/calculators/dftb_ex2_relaxbyDFTB.py
--- 3.24.0-1/doc/ase/calculators/dftb_ex2_relaxbyDFTB.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/dftb_ex2_relaxbyDFTB.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,13 +3,15 @@ from ase.calculators.dftb import Dftb
 from ase.io import read, write
 
 atoms = molecule('H2O')
-calc = Dftb(label='h2o',
-            Driver_='ConjugateGradient',
-            Driver_MaxForceComponent=1e-4,
-            Driver_MaxSteps=1000,
-            Hamiltonian_MaxAngularMomentum_='',
-            Hamiltonian_MaxAngularMomentum_O='p',
-            Hamiltonian_MaxAngularMomentum_H='s')
+calc = Dftb(
+    label='h2o',
+    Driver_='ConjugateGradient',
+    Driver_MaxForceComponent=1e-4,
+    Driver_MaxSteps=1000,
+    Hamiltonian_MaxAngularMomentum_='',
+    Hamiltonian_MaxAngularMomentum_O='p',
+    Hamiltonian_MaxAngularMomentum_H='s',
+)
 
 atoms.calc = calc
 calc.calculate(atoms)
diff -pruN 3.24.0-1/doc/ase/calculators/dftb_ex3_make_2h2o.py 3.26.0-1/doc/ase/calculators/dftb_ex3_make_2h2o.py
--- 3.24.0-1/doc/ase/calculators/dftb_ex3_make_2h2o.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/dftb_ex3_make_2h2o.py	2025-08-12 11:26:23.000000000 +0000
@@ -21,36 +21,40 @@ atoms = o2 + h2_1 + h2_2
 
 # 1fs = 41.3 au
 # 1000K = 0.0031668 au
-calculator_NVE = Dftb(label='h2o',
-                      Hamiltonian_MaxAngularMomentum_='',
-                      Hamiltonian_MaxAngularMomentum_O='p',
-                      Hamiltonian_MaxAngularMomentum_H='s',
-                      Driver_='VelocityVerlet',
-                      Driver_MDRestartFrequency=10,
-                      Driver_Velocities_='',
-                      Driver_Velocities_empty='<<+ "velocities.txt"',
-                      Driver_Steps=1000,
-                      Driver_KeepStationary='Yes',
-                      Driver_TimeStep=4.13,
-                      Driver_Thermostat_='None',
-                      Driver_Thermostat_empty='')
+calculator_NVE = Dftb(
+    label='h2o',
+    Hamiltonian_MaxAngularMomentum_='',
+    Hamiltonian_MaxAngularMomentum_O='p',
+    Hamiltonian_MaxAngularMomentum_H='s',
+    Driver_='VelocityVerlet',
+    Driver_MDRestartFrequency=10,
+    Driver_Velocities_='',
+    Driver_Velocities_empty='<<+ "velocities.txt"',
+    Driver_Steps=1000,
+    Driver_KeepStationary='Yes',
+    Driver_TimeStep=4.13,
+    Driver_Thermostat_='None',
+    Driver_Thermostat_empty='',
+)
 
 # 1fs = 41.3 au
 # 1000K = 0.0031668 au
-calculator_NVT = Dftb(label='h2o',
-                      Hamiltonian_MaxAngularMomentum_='',
-                      Hamiltonian_MaxAngularMomentum_O='p',
-                      Hamiltonian_MaxAngularMomentum_H='s',
-                      Driver_='VelocityVerlet',
-                      Driver_MDRestartFrequency=5,
-                      Driver_Velocities_='',
-                      Driver_Velocities_empty='<<+ "velocities.txt"',
-                      Driver_Steps=500,
-                      Driver_KeepStationary='Yes',
-                      Driver_TimeStep=8.26,
-                      Driver_Thermostat_='Berendsen',
-                      Driver_Thermostat_Temperature=0.00339845142,  # 800 degC
-                      Driver_Thermostat_CouplingStrength=0.01)
+calculator_NVT = Dftb(
+    label='h2o',
+    Hamiltonian_MaxAngularMomentum_='',
+    Hamiltonian_MaxAngularMomentum_O='p',
+    Hamiltonian_MaxAngularMomentum_H='s',
+    Driver_='VelocityVerlet',
+    Driver_MDRestartFrequency=5,
+    Driver_Velocities_='',
+    Driver_Velocities_empty='<<+ "velocities.txt"',
+    Driver_Steps=500,
+    Driver_KeepStationary='Yes',
+    Driver_TimeStep=8.26,
+    Driver_Thermostat_='Berendsen',
+    Driver_Thermostat_Temperature=0.00339845142,  # 800 degC
+    Driver_Thermostat_CouplingStrength=0.01,
+)
 
 write_dftb_velocities(atoms, 'velocities.txt')
 
diff -pruN 3.24.0-1/doc/ase/calculators/dftd3_gpaw.py 3.26.0-1/doc/ase/calculators/dftd3_gpaw.py
--- 3.24.0-1/doc/ase/calculators/dftd3_gpaw.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/dftd3_gpaw.py	2025-08-12 11:26:23.000000000 +0000
@@ -6,11 +6,11 @@ from ase.calculators.dftd3 import DFTD3
 from ase.constraints import UnitCellFilter
 from ase.optimize import LBFGS
 
-np.random.seed(0)
+rng = np.random.RandomState(0)
 
 diamond = bulk('C')
-diamond.rattle(stdev=0.1, seed=0)
-diamond.cell += np.random.normal(scale=0.1, size=(3, 3))
+diamond.rattle(stdev=0.1, rng=rng)
+diamond.cell += rng.normal(scale=0.1, size=(3, 3))
 dft = GPAW(xc='PBE', kpts=(8, 8, 8), mode=PW(400))
 d3 = DFTD3(dft=dft)
 diamond.calc = d3
diff -pruN 3.24.0-1/doc/ase/calculators/elk.rst 3.26.0-1/doc/ase/calculators/elk.rst
--- 3.24.0-1/doc/ase/calculators/elk.rst	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/elk.rst	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,6 @@
+===
+Elk
+===
+
+.. automodule:: ase.calculators.elk
+   :members: ELK
diff -pruN 3.24.0-1/doc/ase/calculators/exciting.py 3.26.0-1/doc/ase/calculators/exciting.py
--- 3.24.0-1/doc/ase/calculators/exciting.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/exciting.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,22 +3,27 @@ from ase.calculators.exciting.exciting i
 
 # test structure, not real
 nitrogen_trioxide_atoms = Atoms(
-    'NO3', cell=[[2, 2, 0], [0, 4, 0], [0, 0, 6]],
+    'NO3',
+    cell=[[2, 2, 0], [0, 4, 0], [0, 0, 6]],
     positions=[(0, 0, 0), (1, 3, 0), (0, 0, 1), (0.5, 0.5, 0.5)],
-    pbc=True)
+    pbc=True,
+)
 
 gs_template_obj = ExcitingGroundStateTemplate()
 
 # Write an exciting input.xml file for the NO3 system.
 gs_template_obj.write_input(
-    directory='./', atoms=nitrogen_trioxide_atoms,
+    directory='./',
+    atoms=nitrogen_trioxide_atoms,
     parameters={
-        "title": None,
-        "species_path": './',
-        "ground_state_input": {
-            "rgkmax": 8.0,
-            "do": "fromscratch",
-            "ngridk": [6, 6, 6],
-            "xctype": "GGA_PBE_SOL",
-            "vkloff": [0, 0, 0]},
-    })
+        'title': None,
+        'species_path': './',
+        'ground_state_input': {
+            'rgkmax': 8.0,
+            'do': 'fromscratch',
+            'ngridk': [6, 6, 6],
+            'xctype': 'GGA_PBE_SOL',
+            'vkloff': [0, 0, 0],
+        },
+    },
+)
diff -pruN 3.24.0-1/doc/ase/calculators/gromacs_example_mm_relax.py 3.26.0-1/doc/ase/calculators/gromacs_example_mm_relax.py
--- 3.24.0-1/doc/ase/calculators/gromacs_example_mm_relax.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/gromacs_example_mm_relax.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,8 +1,8 @@
-""" An example for using gromacs calculator in ase.
-    Atom positions are relaxed.
-    A sample call:
+"""An example for using gromacs calculator in ase.
+ Atom positions are relaxed.
+ A sample call:
 
-   python ./gromacs_example_mm_relax.py his.pdb
+python ./gromacs_example_mm_relax.py his.pdb
 """
 
 import sys
@@ -12,17 +12,15 @@ from ase.calculators.gromacs import Grom
 infile_name = sys.argv[1]
 
 CALC_MM_RELAX = Gromacs(clean=True)
-CALC_MM_RELAX.set_own_params_runs(
-    'extra_pdb2gmx_parameters', '-ignh')
-CALC_MM_RELAX.set_own_params_runs(
-    'init_structure', infile_name)
+CALC_MM_RELAX.set_own_params_runs('extra_pdb2gmx_parameters', '-ignh')
+CALC_MM_RELAX.set_own_params_runs('init_structure', infile_name)
 CALC_MM_RELAX.generate_topology_and_g96file()
 CALC_MM_RELAX.write_input()
 CALC_MM_RELAX.set_own_params_runs(
-    'extra_editconf_parameters', '-bt cubic -c -d 0.8')
+    'extra_editconf_parameters', '-bt cubic -c -d 0.8'
+)
 CALC_MM_RELAX.run_editconf()
-CALC_MM_RELAX.set_own_params_runs(
-    'extra_genbox_parameters', '-cs spc216.gro')
+CALC_MM_RELAX.set_own_params_runs('extra_genbox_parameters', '-cs spc216.gro')
 CALC_MM_RELAX.run_genbox()
 CALC_MM_RELAX.generate_gromacs_run_file()
 CALC_MM_RELAX.run()
diff -pruN 3.24.0-1/doc/ase/calculators/gromacs_example_mm_relax2.py 3.26.0-1/doc/ase/calculators/gromacs_example_mm_relax2.py
--- 3.24.0-1/doc/ase/calculators/gromacs_example_mm_relax2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/gromacs_example_mm_relax2.py	2025-08-12 11:26:23.000000000 +0000
@@ -22,7 +22,8 @@ CALC_MM_RELAX = Gromacs(
     force_field='oplsaa',
     water_model='tip3p',
     base_filename='gromacs_mm-relax',
-    doing_qmmm=False, freeze_qm=True,
+    doing_qmmm=False,
+    freeze_qm=True,
     index_filename='index.ndx',
     extra_mdrun_parameters=' -nt 1 ',
     define='-DFLEXIBLE',
@@ -40,7 +41,8 @@ CALC_MM_RELAX = Gromacs(
     vdwtype='shift',
     rvdw='0.8',
     rvdw_switch='0.75',
-    DispCorr='Ener')
+    DispCorr='Ener',
+)
 CALC_MM_RELAX.generate_topology_and_g96file()
 CALC_MM_RELAX.generate_gromacs_run_file()
 CALC_MM_RELAX.run()
diff -pruN 3.24.0-1/doc/ase/calculators/gulp_example.py 3.26.0-1/doc/ase/calculators/gulp_example.py
--- 3.24.0-1/doc/ase/calculators/gulp_example.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/gulp_example.py	2025-08-12 11:26:23.000000000 +0000
@@ -4,54 +4,57 @@ import numpy as np
 from ase import Atoms
 from ase.calculators.gulp import GULP, Conditions
 
-cluster = Atoms(symbols='O4SiOSiO2SiO2SiO2SiOSiO2SiO3SiO3H8',
-                pbc=np.array([False, False, False], dtype=bool),
-                cell=np.array(
-                    [[0., 0., 0.],
-                     [0., 0., 0.],
-                        [0., 0., 0.]]),
-                positions=np.array(
-                    [[-1.444348, -0.43209, -2.054785],
-                     [-0.236947, 2.98731, 1.200025],
-                        [3.060238, -1.05911, 0.579909],
-                        [2.958277, -3.289076, 2.027579],
-                        [-0.522747, 0.847624, -2.47521],
-                        [-2.830486, -2.7236, -2.020633],
-                        [-0.764328, -1.251141, 1.402431],
-                        [3.334801, 0.041643, -4.168601],
-                        [-1.35204, -2.009562, 0.075892],
-                        [-1.454655, -1.985635, -1.554533],
-                        [0.85504, 0.298129, -3.159972],
-                        [1.75833, 1.256026, 0.690171],
-                        [2.376446, -0.239522, -2.881245],
-                        [1.806515, -4.484208, -2.686456],
-                        [-0.144193, -2.74503, -2.177778],
-                        [0.167583, 1.582976, 0.47998],
-                        [-1.30716, 1.796853, -3.542121],
-                        [1.441364, -3.072993, -1.958788],
-                        [-1.694171, -1.558913, 2.704219],
-                        [4.417516, 1.263796, 0.563573],
-                        [3.066366, 0.49743, 0.071898],
-                        [-0.704497, 0.351869, 1.102318],
-                        [2.958884, 0.51505, -1.556651],
-                        [1.73983, -3.161794, -0.356577],
-                        [2.131519, -2.336982, 0.996026],
-                        [0.752313, -1.788039, 1.687183],
-                        [-0.142347, 1.685301, -1.12086],
-                        [2.32407, -1.845905, -2.588202],
-                        [-2.571557, -1.937877, 2.604727],
-                        [2.556369, -4.551103, -3.2836],
-                        [3.032586, 0.591698, -4.896276],
-                        [-1.67818, 2.640745, -3.27092],
-                        [5.145483, 0.775188, 0.95687],
-                        [-2.81059, -3.4492, -2.650319],
-                        [2.558023, -3.594544, 2.845928],
-                        [0.400993, 3.469148, 1.733289]]))
+cluster = Atoms(
+    symbols='O4SiOSiO2SiO2SiO2SiOSiO2SiO3SiO3H8',
+    pbc=np.array([False, False, False], dtype=bool),
+    cell=np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]),
+    positions=np.array(
+        [
+            [-1.444348, -0.43209, -2.054785],
+            [-0.236947, 2.98731, 1.200025],
+            [3.060238, -1.05911, 0.579909],
+            [2.958277, -3.289076, 2.027579],
+            [-0.522747, 0.847624, -2.47521],
+            [-2.830486, -2.7236, -2.020633],
+            [-0.764328, -1.251141, 1.402431],
+            [3.334801, 0.041643, -4.168601],
+            [-1.35204, -2.009562, 0.075892],
+            [-1.454655, -1.985635, -1.554533],
+            [0.85504, 0.298129, -3.159972],
+            [1.75833, 1.256026, 0.690171],
+            [2.376446, -0.239522, -2.881245],
+            [1.806515, -4.484208, -2.686456],
+            [-0.144193, -2.74503, -2.177778],
+            [0.167583, 1.582976, 0.47998],
+            [-1.30716, 1.796853, -3.542121],
+            [1.441364, -3.072993, -1.958788],
+            [-1.694171, -1.558913, 2.704219],
+            [4.417516, 1.263796, 0.563573],
+            [3.066366, 0.49743, 0.071898],
+            [-0.704497, 0.351869, 1.102318],
+            [2.958884, 0.51505, -1.556651],
+            [1.73983, -3.161794, -0.356577],
+            [2.131519, -2.336982, 0.996026],
+            [0.752313, -1.788039, 1.687183],
+            [-0.142347, 1.685301, -1.12086],
+            [2.32407, -1.845905, -2.588202],
+            [-2.571557, -1.937877, 2.604727],
+            [2.556369, -4.551103, -3.2836],
+            [3.032586, 0.591698, -4.896276],
+            [-1.67818, 2.640745, -3.27092],
+            [5.145483, 0.775188, 0.95687],
+            [-2.81059, -3.4492, -2.650319],
+            [2.558023, -3.594544, 2.845928],
+            [0.400993, 3.469148, 1.733289],
+        ]
+    ),
+)
 
 
 c = Conditions(cluster)
-c.min_distance_rule('O', 'H', ifcloselabel1='O2',
-                    ifcloselabel2='H', elselabel1='O1')
+c.min_distance_rule(
+    'O', 'H', ifcloselabel1='O2', ifcloselabel2='H', elselabel1='O1'
+)
 calc = GULP(keywords='conp', shel=['O1', 'O2'], conditions=c)
 
 # Single point calculation
diff -pruN 3.24.0-1/doc/ase/calculators/octopus_silicon.py 3.26.0-1/doc/ase/calculators/octopus_silicon.py
--- 3.24.0-1/doc/ase/calculators/octopus_silicon.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/octopus_silicon.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,13 +3,15 @@ from ase.calculators.octopus import Octo
 
 system = bulk('Si')
 
-calc = Octopus(directory='oct-si',
-               Spacing=0.25,
-               KPointsGrid=[[4, 4, 4]],
-               KPointsUseSymmetries=True,
-               Output=[['dos'], ['density'], ['potential']],
-               OutputFormat='xcrysden',
-               DosGamma=0.1)
+calc = Octopus(
+    directory='oct-si',
+    Spacing=0.25,
+    KPointsGrid=[[4, 4, 4]],
+    KPointsUseSymmetries=True,
+    Output=[['dos'], ['density'], ['potential']],
+    OutputFormat='xcrysden',
+    DosGamma=0.1,
+)
 
 system.calc = calc
 system.get_potential_energy()
diff -pruN 3.24.0-1/doc/ase/calculators/socketio/example_abinit.py 3.26.0-1/doc/ase/calculators/socketio/example_abinit.py
--- 3.24.0-1/doc/ase/calculators/socketio/example_abinit.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/socketio/example_abinit.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,53 +1,32 @@
 from ase.build import bulk
 from ase.calculators.abinit import Abinit
-from ase.calculators.socketio import SocketIOCalculator
-from ase.constraints import ExpCellFilter
+from ase.filters import FrechetCellFilter
 from ase.optimize import BFGS
 
-atoms = bulk('Si')
-atoms.rattle(stdev=0.1, seed=42)
-
-# Configuration parameters; please edit as appropriate
-pps = '/path/to/pseudopotentials'
+# Modify this line to suit your needs:
 pseudopotentials = {'Si': '14-Si.LDA.fhi'}
-exe = 'abinit'
-
-unixsocket = 'ase_abinit'
-command = f'{exe} PREFIX.in --ipi {unixsocket}:UNIX > PREFIX.log'
-# (In the command, note that PREFIX.in must precede --ipi.)
-
-
-configuration_kwargs = dict(
-    command=command,
-    pp_paths=[pps],
-)
 
+atoms = bulk('Si')
+atoms.rattle(stdev=0.1, seed=42)
 
 # Implementation note: Socket-driven calculations in Abinit inherit several
 # controls for from the ordinary cell optimization code.  We have to hack those
 # variables in order for Abinit not to decide that the calculation converged:
 boilerplate_kwargs = dict(
-    ionmov=28,  # activate i-pi/socket mode
-    expert_user=1,  # Ignore warnings (chksymbreak, chksymtnons, chkdilatmx)
-    optcell=2,  # allow the cell to relax
     tolmxf=1e-300,  # Prevent Abinit from thinking we "converged"
     ntime=100_000,  # Allow "infinitely" many iterations in Abinit
     ecutsm=0.5,  # Smoothing PW cutoff energy (mandatory for cell optimization)
 )
 
-
 kwargs = dict(
     ecut=5 * 27.3,
     tolvrs=1e-8,
     kpts=[2, 2, 2],
     **boilerplate_kwargs,
-    **configuration_kwargs,
 )
 
-abinit = Abinit(**kwargs)
-
-opt = BFGS(ExpCellFilter(atoms),
-           trajectory='opt.traj')
+abinit = Abinit(directory='abinit-calc', **kwargs)
+opt = BFGS(FrechetCellFilter(atoms), trajectory='opt.traj')
 
-with SocketIOCalculator(abinit, unixsocket=unixsocket) as atoms.calc:
+with abinit.socketio(unixsocket='ase-abinit') as atoms.calc:
     opt.run(fmax=0.01)
diff -pruN 3.24.0-1/doc/ase/calculators/socketio/example_aims.py 3.26.0-1/doc/ase/calculators/socketio/example_aims.py
--- 3.24.0-1/doc/ase/calculators/socketio/example_aims.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/socketio/example_aims.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,15 +1,9 @@
-import sys
-
 from ase.build import molecule
 from ase.calculators.aims import Aims
-from ase.calculators.socketio import SocketIOCalculator
 from ase.optimize import BFGS
 
-# Environment-dependent parameters -- please configure according to machine
 # Note that FHI-aim support for the i-PI protocol must be specifically
 # enabled at compile time, e.g.: make -f Makefile.ipi ipi.mpi
-species_dir = '/home/aimsuser/src/fhi-aims.171221_1/species_defaults/light'
-command = 'ipi.aims.171221_1.mpi.x'
 
 # This example uses INET; see other examples for how to use UNIX sockets.
 port = 31415
@@ -17,18 +11,10 @@ port = 31415
 atoms = molecule('H2O', vacuum=3.0)
 atoms.rattle(stdev=0.1)
 
-aims = Aims(command=command,
-            use_pimd_wrapper=('localhost', port),
-            # alternative: ('UNIX:mysocketname', 31415)
-            # (numeric port must be given even with Unix socket)
-            compute_forces=True,
-            xc='LDA',
-            species_dir=species_dir)
-
+aims = Aims(directory='aims-calc', xc='LDA')
 opt = BFGS(atoms, trajectory='opt.aims.traj', logfile='opt.aims.log')
 
-with SocketIOCalculator(aims, log=sys.stdout, port=port) as calc:
-    # For running with UNIX socket, put unixsocket='mysocketname'
-    # instead of port cf. aims parameters above
-    atoms.calc = calc
+# For running with UNIX socket, put unixsocket='mysocketname'
+# instead of port cf. aims parameters above
+with aims.socketio(port=port) as atoms.calc:
     opt.run(fmax=0.05)
diff -pruN 3.24.0-1/doc/ase/calculators/socketio/example_client_gpaw.py 3.26.0-1/doc/ase/calculators/socketio/example_client_gpaw.py
--- 3.24.0-1/doc/ase/calculators/socketio/example_client_gpaw.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/socketio/example_client_gpaw.py	2025-08-12 11:26:23.000000000 +0000
@@ -8,10 +8,12 @@ from ase.io import read
 atoms = read('initial.traj')
 unixsocket = 'ase_server_socket'
 
-atoms.calc = GPAW(mode='lcao',
-                  basis='dzp',
-                  txt='gpaw.client.txt',
-                  mixer=Mixer(0.7, 7, 20.0))
+atoms.calc = GPAW(
+    mode='lcao',
+    basis='dzp',
+    txt='gpaw.client.txt',
+    mixer=Mixer(0.7, 7, 20.0),
+)
 
 client = SocketClient(unixsocket=unixsocket)
 
diff -pruN 3.24.0-1/doc/ase/calculators/socketio/example_dftb.py 3.26.0-1/doc/ase/calculators/socketio/example_dftb.py
--- 3.24.0-1/doc/ase/calculators/socketio/example_dftb.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/socketio/example_dftb.py	2025-08-12 11:26:23.000000000 +0000
@@ -5,16 +5,29 @@ from ase.calculators.dftb import Dftb
 from ase.calculators.socketio import SocketIOCalculator
 from ase.optimize import BFGS
 
+# DFTB must be compiled with socket support in order for this to work:
+# "cmake -D WITH_SOCKETS=TRUE ..."
+socketfile = 'Hello'
+
+socket_kwargs = dict(
+    Driver_='',
+    Driver_Socket_='',
+    Driver_Socket_File=socketfile,
+)
+
 atoms = molecule('H2O')
-dftb = Dftb(Hamiltonian_MaxAngularMomentum_='',
-            Hamiltonian_MaxAngularMomentum_O='"p"',
-            Hamiltonian_MaxAngularMomentum_H='"s"',
-            Driver_='',
-            Driver_Socket_='',
-            Driver_Socket_File='Hello')
-opt = BFGS(atoms, trajectory='test.traj')
+dftb = Dftb(
+    directory='dftb-calc',
+    Hamiltonian_MaxAngularMomentum_='',
+    Hamiltonian_MaxAngularMomentum_O='"p"',
+    Hamiltonian_MaxAngularMomentum_H='"s"',
+    **socket_kwargs,
+)
 
+opt = BFGS(atoms, trajectory='opt.traj')
 
-with SocketIOCalculator(dftb, log=sys.stdout, unixsocket='Hello') as calc:
+# dftb does not have a .socketio() shortcut method, so we create the socketio
+# calculator manually (and must remember specify the same socket file):
+with SocketIOCalculator(dftb, log=sys.stdout, unixsocket=socketfile) as calc:
     atoms.calc = calc
     opt.run(fmax=0.01)
diff -pruN 3.24.0-1/doc/ase/calculators/socketio/example_espresso.py 3.26.0-1/doc/ase/calculators/socketio/example_espresso.py
--- 3.24.0-1/doc/ase/calculators/socketio/example_espresso.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/socketio/example_espresso.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,46 +1,29 @@
-import sys
-
 from ase.build import molecule
 from ase.calculators.espresso import Espresso
-from ase.calculators.socketio import SocketIOCalculator
 from ase.optimize import BFGS
 
+# Update this line so it works with your configuration:
+pseudopotentials = {'H': 'h_lda_v1.4.uspp.F.UPF', 'O': 'o_lda_v1.2.uspp.F.UPF'}
+
 atoms = molecule('H2O', vacuum=3.0)
 atoms.rattle(stdev=0.1)
 
-# Environment-dependent parameters (please configure before running):
-pseudopotentials = {'H': 'H.pbe-rrkjus.UPF',
-                    'O': 'O.pbe-rrkjus.UPF'}
-pseudo_dir = '.'
+espresso = Espresso(
+    directory='calc-espresso',
+    ecutwfc=30.0,
+    pseudopotentials=pseudopotentials,
+)
+
+opt = BFGS(atoms, trajectory='opt.traj', logfile='opt.log')
 
 # In this example we use a UNIX socket.  See other examples for INET socket.
 # UNIX sockets are faster then INET sockets, but cannot run over a network.
 # UNIX sockets are files.  The actual path will become /tmp/ipi_ase_espresso.
-unixsocket = 'ase_espresso'
-
-# Configure pw.x command for UNIX or INET.
-#
-# UNIX: --ipi {unixsocket}:UNIX
-# INET: --ipi {host}:{port}
 #
 # See also QE documentation, e.g.:
 #
 #    https://www.quantum-espresso.org/Doc/pw_user_guide/node13.html
-#
-command = ('pw.x < PREFIX.pwi --ipi {unixsocket}:UNIX > PREFIX.pwo'
-           .format(unixsocket=unixsocket))
-
-espresso = Espresso(command=command,
-                    ecutwfc=30.0,
-                    pseudopotentials=pseudopotentials,
-                    pseudo_dir=pseudo_dir)
-
-opt = BFGS(atoms, trajectory='opt.traj',
-           logfile='opt.log')
-
-with SocketIOCalculator(espresso, log=sys.stdout,
-                        unixsocket=unixsocket) as calc:
-    atoms.calc = calc
+with espresso.socketio(unixsocket='ase-espresso') as atoms.calc:
     opt.run(fmax=0.05)
 
 # Note: QE does not generally quit cleanly - expect nonzero exit codes.
diff -pruN 3.24.0-1/doc/ase/calculators/socketio/example_nwchem.py 3.26.0-1/doc/ase/calculators/socketio/example_nwchem.py
--- 3.24.0-1/doc/ase/calculators/socketio/example_nwchem.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/socketio/example_nwchem.py	2025-08-12 11:26:23.000000000 +0000
@@ -9,15 +9,13 @@ atoms = molecule('H2O')
 atoms.rattle(stdev=0.1)
 
 unixsocket = 'ase_nwchem'
+socket_kwargs = dict(task='optimize', driver={'socket': {'unix': unixsocket}})
+nwchem = NWChem(directory='calc-nwchem', theory='scf', **socket_kwargs)
 
-nwchem = NWChem(theory='scf',
-                task='optimize',
-                driver={'socket': {'unix': unixsocket}})
+opt = BFGS(atoms, trajectory='opt.traj', logfile='opt.log')
 
-opt = BFGS(atoms, trajectory='opt.traj',
-           logfile='opt.log')
-
-with SocketIOCalculator(nwchem, log=sys.stdout,
-                        unixsocket=unixsocket) as calc:
+# Manually create socket io calcualtor since nwchem does not yet have
+# the .socketio() shortcut:
+with SocketIOCalculator(nwchem, log=sys.stdout, unixsocket=unixsocket) as calc:
     atoms.calc = calc
     opt.run(fmax=0.05)
diff -pruN 3.24.0-1/doc/ase/calculators/socketio/example_server.py 3.26.0-1/doc/ase/calculators/socketio/example_server.py
--- 3.24.0-1/doc/ase/calculators/socketio/example_server.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/socketio/example_server.py	2025-08-12 11:26:23.000000000 +0000
@@ -13,8 +13,7 @@ write('initial.traj', atoms)
 
 opt = BFGS(atoms, trajectory='opt.driver.traj', logfile='opt.driver.log')
 
-with SocketIOCalculator(log=sys.stdout,
-                        unixsocket=unixsocket) as calc:
+with SocketIOCalculator(log=sys.stdout, unixsocket=unixsocket) as calc:
     # Server is now running and waiting for connections.
     # If you want to launch the client process here directly,
     # instead of manually in the terminal, uncomment these lines:
diff -pruN 3.24.0-1/doc/ase/calculators/socketio/example_siesta.py 3.26.0-1/doc/ase/calculators/socketio/example_siesta.py
--- 3.24.0-1/doc/ase/calculators/socketio/example_siesta.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/socketio/example_siesta.py	2025-08-12 11:26:23.000000000 +0000
@@ -7,11 +7,13 @@ from ase.optimize import BFGS
 
 unixsocket = 'siesta'
 
-fdf_arguments = {'MD.TypeOfRun': 'Master',
-                 'Master.code': 'i-pi',
-                 'Master.interface': 'socket',
-                 'Master.address': unixsocket,
-                 'Master.socketType': 'unix'}
+fdf_arguments = {
+    'MD.TypeOfRun': 'Master',
+    'Master.code': 'i-pi',
+    'Master.interface': 'socket',
+    'Master.address': unixsocket,
+    'Master.socketType': 'unix',
+}
 
 # To connect through INET socket instead, use:
 #   fdf_arguments['Master.port'] = port
@@ -22,11 +24,10 @@ fdf_arguments = {'MD.TypeOfRun': 'Master
 atoms = molecule('H2O', vacuum=3.0)
 atoms.rattle(stdev=0.1)
 
-siesta = Siesta(fdf_arguments=fdf_arguments)
+siesta = Siesta(directory='siesta-calc', fdf_arguments=fdf_arguments)
 opt = BFGS(atoms, trajectory='opt.siesta.traj', logfile='opt.siesta.log')
 
-with SocketIOCalculator(siesta, log=sys.stdout,
-                        unixsocket=unixsocket) as calc:
+with SocketIOCalculator(siesta, log=sys.stdout, unixsocket=unixsocket) as calc:
     atoms.calc = calc
     opt.run(fmax=0.05)
 
diff -pruN 3.24.0-1/doc/ase/calculators/socketio/socketio.rst 3.26.0-1/doc/ase/calculators/socketio/socketio.rst
--- 3.24.0-1/doc/ase/calculators/socketio/socketio.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/socketio/socketio.rst	2025-08-12 11:26:23.000000000 +0000
@@ -62,7 +62,14 @@ details.  The i-PI documentation may als
 How to use the ASE socket I/O interface
 ---------------------------------------
 
-Example using Quantum Espresso
+The following examples should work if ASE is configured with each of the
+calculators in question, and if the pseudopotentials are available.
+
+Use ``ase info --calculators`` to see which calculators are installed
+and make sure your configuration works for the calculator in question.
+Then the following examples should work out of the box as long as
+you adapt the specifications of pseudopotential files to match
+your configuration.
 
 .. literalinclude:: example_espresso.py
 
diff -pruN 3.24.0-1/doc/ase/calculators/tersoff.rst 3.26.0-1/doc/ase/calculators/tersoff.rst
--- 3.24.0-1/doc/ase/calculators/tersoff.rst	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/tersoff.rst	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,201 @@
+.. module:: ase.calculators.tersoff
+    :synopsis: Tersoff Interatomic Potential
+
+Tersoff Calculator
+==================
+
+The Tersoff potential is a three-body empirical potential that can model covalently
+bonded solids like silicon or carbon. This implementation provides a native ASE
+calculator that follows the `LAMMPS-style Tersoff
+<https://docs.lammps.org/pair_tersoff.html>`_ parameterization.
+
+.. note::
+
+    The performance of the routines for this calculator have not been optimized for
+    speed nor benchmarked with the LAMMPS implementation.
+
+Theory
+------
+
+The many-body interaction is based on bond-order concept that includes both two-body and
+three-body terms. The total energy is given by:
+
+.. math::
+
+    \begin{split}
+    E & = \frac{1}{2} \sum_i \sum_{j \neq i} V_{ij} \\
+    V_{ij} & = f_C(r_{ij}) \left[ f_R(r_{ij}) + b_{ij} f_A(r_{ij}) \right]
+    \end{split}
+
+where the repulsive and attractive pair terms are:
+
+.. math::
+
+    \begin{split}
+    f_R(r) & = A \exp (-\lambda_1 r) \\
+    f_A(r) & = -B \exp (-\lambda_2 r)
+    \end{split}
+
+The cutoff function :math:`f_C(r)` ensures smooth decay of interactions:
+
+.. math::
+
+    f_C(r) = \begin{cases}
+    1 & r < R - D \\
+    \frac{1}{2} - \frac{1}{2} \sin \left( \frac{\pi}{2} \frac{r-R}{D} \right) & R-D < r < R + D \\
+    0 & r > R + D
+    \end{cases}
+
+The bond order term :math:`b_{ij}` captures the short-range local atomic environment:
+
+.. math::
+
+    \begin{split}
+    b_{ij} & = \left( 1 + \beta^n {\zeta_{ij}}^n \right)^{-\frac{1}{2n}} \\
+    \zeta_{ij} & = \sum_{k \neq i,j} f_C(r_{ik}) g(\theta_{ijk})
+                  \exp \left[ {\lambda_3}^m (r_{ij} - r_{ik})^m \right]
+    \end{split}
+
+where :math:`\theta_{ijk}` is the angle between bonds :math:`ij` and :math:`ik`. The
+angular function :math:`g(\theta)` is:
+
+.. math::
+
+    g(\theta) = \gamma \left( 1 + \frac{c^2}{d^2} -
+                 \frac{c^2}{d^2 + (h - \cos \theta)^2} \right)
+
+where :math:`h = \cos \theta_0` defines the preferred angle :math:`\theta_0`.
+
+The parameters :math:`A`, :math:`B`, :math:`\lambda_1`, :math:`\lambda_2`,
+:math:`\lambda_3`, :math:`\beta`, :math:`n`, :math:`c`, :math:`d`, :math:`h`,
+:math:`\gamma`, :math:`m`, :math:`R`, and :math:`D` define the potential for each
+interaction type.
+
+For a complete description of the functional forms and parameters, see the
+:mod:`ase.calculators.tersoff` module documentation.
+
+Parameters
+----------
+
+The Tersoff potential is defined by 14 parameters for each three-body interaction:
+
+========= ===================================
+Parameter Description
+========= ===================================
+A         Repulsive pair potential prefactor
+B         Attractive pair potential prefactor
+lambda1   Decay length for repulsive term
+lambda2   Decay length for attractive term
+lambda3   Decay length for angular term
+beta      Angular strength parameter
+n         Angular exponent
+c         Angular coefficient
+d         Angular parameter
+h         Cosine of angle parameter
+gamma     Angular scaling
+m         Bond order exponent
+R         Cutoff distance
+D         Cutoff width
+========= ===================================
+
+See :class:`ase.calculators.tersoff.TersoffParameters` for details.
+
+Examples
+--------
+
+Silicon Diamond Structure
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Using calculator on silicon crystal in the diamond structure:
+
+.. code-block:: python
+
+    from ase.build import bulk
+    from ase.calculators.tersoff import Tersoff, TersoffParameters
+
+    # Create silicon diamond structure
+    si = bulk("Si", "diamond", a=5.43)
+
+    # Define Tersoff parameters for Si
+    si_params = {
+        ("Si", "Si", "Si"): TersoffParameters(
+            A=3264.7,
+            B=95.373,
+            lambda1=3.2394,
+            lambda2=1.3258,
+            lambda3=1.3258,
+            beta=0.33675,
+            gamma=1.00,
+            m=3.00,
+            n=22.956,
+            c=4.8381,
+            d=2.0417,
+            h=0.0000,
+            R=3.00,
+            D=0.20,
+        )
+    }
+
+    # Set up calculator
+    calc = Tersoff(si_params)
+    si.calc = calc
+
+    # Calculate properties
+    energy = si.get_potential_energy()
+    forces = si.get_forces()
+    stress = si.get_stress()
+
+Parameter Updates
+~~~~~~~~~~~~~~~~~
+
+The calculator parameters can be updated after initialization:
+
+.. code-block:: python
+
+    # Update single parameters
+    calc.set_parameters(("Si", "Si", "Si"), R=2.9, D=0.25)
+
+    # Or replace entire parameter set
+    new_params = TersoffParameters(...)
+    calc.set_parameters(("Si", "Si", "Si"), params=new_params)
+
+Interface to LAMMPS Files
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. warning::
+
+    This interface has only been tested with `Si.tersoff
+    <https://github.com/lammps/lammps/blob/ea1607f1d8a70941cc675f18c4255c25c95c459f/potentials/Si.tersoff>`_
+    and `SiC.tersoff
+    <https://github.com/lammps/lammps/blob/ea1607f1d8a70941cc675f18c4255c25c95c459f/potentials/SiC.tersoff>`_
+    LAMMPS files so it is not necessarily guaranteed to parse all LAMMPS Tersoff files. Therefore, it is
+    recommended to format the LAMMPS Tersoff file using similar to the *Si.tersoff* or
+    *SiC.tersoff* files.
+
+Read parameters from a `LAMMPS-style Tersoff file
+<https://docs.lammps.org/pair_tersoff.html>`_:
+
+.. code-block:: python
+
+    # Initialize from LAMMPS file
+    calc = Tersoff.from_lammps("SiC.tersoff")
+
+Tersoff Calculator Class
+++++++++++++++++++++++++
+
+.. autoclass:: ase.calculators.tersoff.TersoffParameters
+    :class-doc-from: class
+
+.. autoclass:: ase.calculators.tersoff.Tersoff
+    :members: from_lammps, read_lammps_format, set_parameters
+
+References
+----------
+
+1. J. Tersoff, "New empirical approach for the structure and energy of covalent
+      systems", *Phys. Rev. B* **37**, 6991 (1988)
+2. J. Tersoff, "Modeling solid-state chemistry: Interatomic potentials for
+      multicomponent systems", *Phys. Rev. B* **39**, 5566(R) (1989)
+3. S. J. Plimpton, A. Kohlmeyer, A. P. Thompson, et al., "LAMMPS: Large-scale
+         Atomic/Molecular Massively Parallel Simulator". Zenodo, Aug. 02, 2023.
+         `doi:10.5281/zenodo.10806852 <https://doi.org/10.5281/zenodo.10806852>`_.
diff -pruN 3.24.0-1/doc/ase/calculators/vasp_si_bandstructure.py 3.26.0-1/doc/ase/calculators/vasp_si_bandstructure.py
--- 3.24.0-1/doc/ase/calculators/vasp_si_bandstructure.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/calculators/vasp_si_bandstructure.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,5 +1,6 @@
 # creates: vasp_si_bandstructure.png
-# flake8: noqa
+# fmt: off
+
 import numpy as np
 
 from ase.build import bulk
diff -pruN 3.24.0-1/doc/ase/cluster/cluster.py 3.26.0-1/doc/ase/cluster/cluster.py
--- 3.24.0-1/doc/ase/cluster/cluster.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/cluster/cluster.py	2025-08-12 11:26:23.000000000 +0000
@@ -9,16 +9,14 @@ lc = 3.61000
 culayer = FaceCenteredCubic('Cu', surfaces, layers, latticeconstant=lc)
 culayer.rotate(6, 'x', rotate_cell=True)
 culayer.rotate(2, 'y', rotate_cell=True)
-write('culayer.pov', culayer,
-      show_unit_cell=0).render()
+write('culayer.pov', culayer, show_unit_cell=0).render()
 
 surfaces = [(1, 0, 0), (1, 1, 1), (1, -1, 1)]
 layers = [6, 5, -1]
 trunc = FaceCenteredCubic('Cu', surfaces, layers)
 trunc.rotate(6, 'x', rotate_cell=True)
 trunc.rotate(2, 'y', rotate_cell=True)
-write('truncated.pov', trunc,
-      show_unit_cell=0).render()
+write('truncated.pov', trunc, show_unit_cell=0).render()
 
 # This does not work!
 # surfaces = [(0, 0, 0, 1), (1, 1, -2, 0), (1, 0, -1, 1)]
diff -pruN 3.24.0-1/doc/ase/db/db.py 3.26.0-1/doc/ase/db/db.py
--- 3.24.0-1/doc/ase/db/db.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/db/db.py	2025-08-12 11:26:23.000000000 +0000
@@ -32,8 +32,7 @@ with open('ase-db.txt', 'w') as fd:
     fd.write(output.decode())
 with open('ase-db-long.txt', 'w') as fd:
     fd.write('$ ase db abc.db relaxed=1 -l\n')
-    output = subprocess.check_output(
-        ['ase', 'db', 'abc.db', 'relaxed=1', '-l'])
+    output = subprocess.check_output(['ase', 'db', 'abc.db', 'relaxed=1', '-l'])
     fd.write(output.decode())
 
 row = c.get(relaxed=1, calculator='emt')
@@ -58,5 +57,9 @@ with open('known-keys.csv', 'w') as fd:
         unit = keydesc.unit
         if unit == '|e|':
             unit = r'\|e|'
-        print('{},{},{},{}'.format(
-            key, keydesc.shortdesc, keydesc.longdesc, unit), file=fd)
+        print(
+            '{},{},{},{}'.format(
+                key, keydesc.shortdesc, keydesc.longdesc, unit
+            ),
+            file=fd,
+        )
diff -pruN 3.24.0-1/doc/ase/db/db.rst 3.26.0-1/doc/ase/db/db.rst
--- 3.24.0-1/doc/ase/db/db.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/db/db.rst	2025-08-12 11:26:23.000000000 +0000
@@ -24,6 +24,12 @@ MariaDB_:
 The JSON and SQLite3 back-ends work "out of the box", whereas PostgreSQL, MySQL
 and MariaDB requires a server (See :ref:`server` or :ref:`MySQL_server`).
 
+.. note::
+
+   You need to install the back-ends for PostgreSQL, MySQL and MariaDB with::
+
+      pip install git@gitlab.com:ase/ase-db-backends
+
 There is a command-line tool called :ref:`ase-db` that can be
 used to query and manipulate databases and also a `Python interface`_.
 
@@ -77,6 +83,8 @@ Show all details for a single row:
 .. seealso::
 
     * :ref:`cli`
+    * `texase <https://github.com/steenlysgaard/texase>`_ is a
+      terminal user interface (TUI) for ASE databases
 
 
 Querying
diff -pruN 3.24.0-1/doc/ase/dft/bs.py 3.26.0-1/doc/ase/dft/bs.py
--- 3.24.0-1/doc/ase/dft/bs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/dft/bs.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,8 +3,7 @@ from ase.build import bulk
 from ase.calculators.test import FreeElectrons
 
 a = bulk('Cu')
-a.calc = FreeElectrons(nvalence=1,
-                       kpts={'path': 'GXWLGK', 'npoints': 200})
+a.calc = FreeElectrons(nvalence=1, kpts={'path': 'GXWLGK', 'npoints': 200})
 a.get_potential_energy()
 bs = a.calc.band_structure()
 bs.plot(emin=0, emax=20, filename='cu.png')
diff -pruN 3.24.0-1/doc/ase/dft/bz.py 3.26.0-1/doc/ase/dft/bz.py
--- 3.24.0-1/doc/ase/dft/bz.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/dft/bz.py	2025-08-12 11:26:23.000000000 +0000
@@ -31,10 +31,12 @@ with open('bztable.rst', 'w') as fd:
     for i, lat in enumerate(all_variants()):
         id = f'{i:02d}.{lat.variant}'
         imagefname = f'{id}.svg'
-        txt = entry.format(name=lat.variant,
-                           longname=lat.longname,
-                           bandpath=lat.bandpath().path,
-                           fname=imagefname)
+        txt = entry.format(
+            name=lat.variant,
+            longname=lat.longname,
+            bandpath=lat.bandpath().path,
+            fname=imagefname,
+        )
         print(txt, file=fd)
         ax = lat.plot_bz()
         fig = ax.get_figure()
diff -pruN 3.24.0-1/doc/ase/filters.rst 3.26.0-1/doc/ase/filters.rst
--- 3.24.0-1/doc/ase/filters.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/filters.rst	2025-08-12 11:26:23.000000000 +0000
@@ -33,14 +33,15 @@ being true if the atom should be kept vi
 
 Example of use::
 
-  >>> from ase import Atoms, Filter
+  >>> from ase import Atoms
+  >>> from ase.filters import Filter
   >>> atoms=Atoms(positions=[[ 0    , 0    , 0],
   ...                        [ 0.773, 0.600, 0],
   ...                        [-0.773, 0.600, 0]],
   ...             symbols='OH2')
   >>> f1 = Filter(atoms, indices=[1, 2])
   >>> f2 = Filter(atoms, mask=[0, 1, 1])
-  >>> f3 = Filter(atoms, mask=[a.Z == 1 for a in atoms])
+  >>> f3 = Filter(atoms, mask=[a.z == 1 for a in atoms])
   >>> f1.get_positions()
   [[ 0.773  0.6    0.   ]
    [-0.773  0.6    0.   ]]
diff -pruN 3.24.0-1/doc/ase/fix_symmetry_example.py 3.26.0-1/doc/ase/fix_symmetry_example.py
--- 3.24.0-1/doc/ase/fix_symmetry_example.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/fix_symmetry_example.py	2025-08-12 11:26:23.000000000 +0000
@@ -20,9 +20,9 @@ atoms_unsym.calc = LennardJones()
 ucf_unsym = UnitCellFilter(atoms_unsym)
 
 dyn = BFGS(ucf_unsym)
-print("Initial Energy", atoms_unsym.get_potential_energy())
+print('Initial Energy', atoms_unsym.get_potential_energy())
 dyn.run(fmax=0.001)
-print("Final Energy", atoms_unsym.get_potential_energy())
+print('Final Energy', atoms_unsym.get_potential_energy())
 
 # Now we repeat the optimisation with the symmetrization constraint in place
 atoms_sym = atoms_init.copy()
@@ -31,23 +31,25 @@ atoms_sym.set_constraint(FixSymmetry(ato
 ucf_sym = UnitCellFilter(atoms_sym)
 
 dyn = BFGS(ucf_sym)
-print("Initial Energy", atoms_sym.get_potential_energy())
+print('Initial Energy', atoms_sym.get_potential_energy())
 dyn.run(fmax=0.001)
-print("Final Energy", atoms_sym.get_potential_energy())
+print('Final Energy', atoms_sym.get_potential_energy())
 
-print("position difference", np.linalg.norm(atoms_unsym.get_positions() -
-                                            atoms_sym.get_positions()))
+print(
+    'position difference',
+    np.linalg.norm(atoms_unsym.get_positions() - atoms_sym.get_positions()),
+)
 
 # We print out the initial symmetry groups at two different precision levels
-print("initial symmetry at precision 1e-6")
+print('initial symmetry at precision 1e-6')
 check_symmetry(atoms_init, 1.0e-6, verbose=True)
-print("initial symmetry at precision 1e-8")
+print('initial symmetry at precision 1e-8')
 check_symmetry(atoms_init, 1.0e-8, verbose=True)
 
 # Printing the final symmetries shows that
 # the "unsym" case relaxes to a lower energy fcc structure
 # with a change in spacegroup, while the "sym" case stays as bcc
-print("unsym symmetry after relaxation")
+print('unsym symmetry after relaxation')
 d_unsym = check_symmetry(atoms_unsym, verbose=True)
-print("sym symmetry after relaxation")
+print('sym symmetry after relaxation')
 d_sym = check_symmetry(atoms_sym, verbose=True)
diff -pruN 3.24.0-1/doc/ase/formats.py 3.26.0-1/doc/ase/formats.py
--- 3.24.0-1/doc/ase/formats.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/formats.py	2025-08-12 11:26:23.000000000 +0000
@@ -2,8 +2,18 @@
 from ase.formula import Formula
 
 formulas = ['H2O', 'SiC', 'MoS2', 'AB2', 'BN', 'SiO2']
-formats = ['hill', 'metal', 'abc', 'reduce', 'ab2', 'a2b', 'periodic',
-           'latex', 'html', 'rest']
+formats = [
+    'hill',
+    'metal',
+    'abc',
+    'reduce',
+    'ab2',
+    'a2b',
+    'periodic',
+    'latex',
+    'html',
+    'rest',
+]
 
 
 with open('formats.csv', 'w') as fd:
diff -pruN 3.24.0-1/doc/ase/io/io_csv.py 3.26.0-1/doc/ase/io/io_csv.py
--- 3.24.0-1/doc/ase/io/io_csv.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/io/io_csv.py	2025-08-12 11:26:23.000000000 +0000
@@ -12,5 +12,4 @@ with open('io.csv', 'w') as fd:
             c += 'W'
         if not io.single:
             c += '+'
-        print(f':ref:`{format}`, {all_formats[format][0]}, {c}',
-              file=fd)
+        print(f':ref:`{format}`, {all_formats[format][0]}, {c}', file=fd)
diff -pruN 3.24.0-1/doc/ase/io/io_formats.py 3.26.0-1/doc/ase/io/io_formats.py
--- 3.24.0-1/doc/ase/io/io_formats.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/io/io_formats.py	2025-08-12 11:26:23.000000000 +0000
@@ -8,22 +8,31 @@ with open('formatoptions.rst', 'w') as f
     format_names = sorted(all_formats.keys())
     for name in format_names:
         fmt = all_formats[name]
-        print(f".. _{name}:\n", file=fd)
+        print(f'.. _{name}:\n', file=fd)
         print(name, file=fd)
         print('----------------------------------------', file=fd)
         if fmt.can_read:
-            print('\n .. autofunction:: {:}.read_{:}\n\n'.format(
-                fmt.module_name, fmt._formatname), file=fd)
+            print(
+                '\n .. autofunction:: {:}.read_{:}\n\n'.format(
+                    fmt.module_name, fmt._formatname
+                ),
+                file=fd,
+            )
         if fmt.can_write:
-            print('\n .. autofunction:: {:}.write_{:}\n\n'.format(
-                fmt.module_name, fmt._formatname), file=fd)
+            print(
+                '\n .. autofunction:: {:}.write_{:}\n\n'.format(
+                    fmt.module_name, fmt._formatname
+                ),
+                file=fd,
+            )
         if (not fmt.can_read) and (not fmt.can_write):
-            print('\n No automatic documentation of this module found.',
-                  file=fd)
+            print(
+                '\n No automatic documentation of this module found.', file=fd
+            )
             print(
                 '\n Visit '
                 'https://gitlab.com/ase/ase/tree/master/ase/io/{:}.py'
                 ' to see if you find the information needed in '
-                'the source code.\n\n'.format(
-                    fmt.module_name),
-                file=fd)
+                'the source code.\n\n'.format(fmt.module_name),
+                file=fd,
+            )
diff -pruN 3.24.0-1/doc/ase/io/iopng.py 3.26.0-1/doc/ase/io/iopng.py
--- 3.24.0-1/doc/ase/io/iopng.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/io/iopng.py	2025-08-12 11:26:23.000000000 +0000
@@ -11,12 +11,19 @@ slab = fcc111('Cu', (2, 2, 3), a=a, vacu
 add_adsorbate(slab, adsorbate, 1.8, 'ontop')
 
 write('io1.png', slab * (3, 3, 1), rotation='10z,-80x')
-write('io2.pov', slab * (3, 3, 1), rotation='10z,-80x', povray_settings=dict(
-      transparent=False)).render()
+write(
+    'io2.pov',
+    slab * (3, 3, 1),
+    rotation='10z,-80x',
+    povray_settings=dict(transparent=False),
+).render()
 d = a / 2**0.5
-write('io3.pov', slab * (2, 2, 1),
-      bbox=(d, 0, 3 * d, d * 3**0.5),
-      povray_settings=dict(transparent=False)).render()
+write(
+    'io3.pov',
+    slab * (2, 2, 1),
+    bbox=(d, 0, 3 * d, d * 3**0.5),
+    povray_settings=dict(transparent=False),
+).render()
 
 write('slab.traj', slab)
 b = read('slab.traj')
diff -pruN 3.24.0-1/doc/ase/io/save_C2H4.py 3.26.0-1/doc/ase/io/save_C2H4.py
--- 3.24.0-1/doc/ase/io/save_C2H4.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/io/save_C2H4.py	2025-08-12 11:26:23.000000000 +0000
@@ -11,8 +11,13 @@ high_bondorder_pairs = {}
 high_bondorder_pairs[(0, 1)] = ((0, 0, 0), 2, (0.17, 0.17, 0))
 bondpairs = set_high_bondorder_pairs(bondpairs, high_bondorder_pairs)
 
-renderer = write('C2H4.pov', C2H4, format='pov',
-                 radii=r, rotation='90y',
-                 povray_settings=dict(canvas_width=200, bondatoms=bondpairs))
+renderer = write(
+    'C2H4.pov',
+    C2H4,
+    format='pov',
+    radii=r,
+    rotation='90y',
+    povray_settings=dict(canvas_width=200, bondatoms=bondpairs),
+)
 
 renderer.render()
diff -pruN 3.24.0-1/doc/ase/io/save_pov.py 3.26.0-1/doc/ase/io/save_pov.py
--- 3.24.0-1/doc/ase/io/save_pov.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/io/save_pov.py	2025-08-12 11:26:23.000000000 +0000
@@ -8,11 +8,17 @@ from ase.io import write
 
 a = 5.64  # Lattice constant for NaCl
 cell = [a / np.sqrt(2), a / np.sqrt(2), a]
-atoms = Atoms(symbols='Na2Cl2', pbc=True, cell=cell,
-              scaled_positions=[(.0, .0, .0),
-                                (.5, .5, .5),
-                                (.5, .5, .0),
-                                (.0, .0, .5)]) * (3, 4, 2) + molecule('C6H6')
+atoms = Atoms(
+    symbols='Na2Cl2',
+    pbc=True,
+    cell=cell,
+    scaled_positions=[
+        (0.0, 0.0, 0.0),
+        (0.5, 0.5, 0.5),
+        (0.5, 0.5, 0.0),
+        (0.0, 0.0, 0.5),
+    ],
+) * (3, 4, 2) + molecule('C6H6')
 
 # Move molecule to 3.5Ang from surface, and translate one unit cell in xy
 atoms.positions[-12:, 2] += atoms.positions[:-12, 2].max() + 3.5
@@ -28,9 +34,9 @@ rot = '35x,63y,36z'  # found using ag: '
 # Common kwargs for eps, png, pov
 generic_projection_settings = {
     'rotation': rot,  # text string with rotation (default='' )
-    'radii': .85,  # float, or a list with one float per atom
+    'radii': 0.85,  # float, or a list with one float per atom
     'colors': None,  # List: one (r, g, b) tuple per atom
-    'show_unit_cell': 2,   # 0, 1, or 2 to not show, show, and show all of cell
+    'show_unit_cell': 2,  # 0, 1, or 2 to not show, show, and show all of cell
 }
 
 # Extra kwargs only available for povray (All units in angstrom)
@@ -40,22 +46,30 @@ povray_settings = {
     'transparent': False,  # Transparent background
     'canvas_width': None,  # Width of canvas in pixels
     'canvas_height': None,  # Height of canvas in pixels
-    'camera_dist': 50.,  # Distance from camera to front atom
+    'camera_dist': 50.0,  # Distance from camera to front atom
     'image_plane': None,  # Distance from front atom to image plane
     'camera_type': 'perspective',  # perspective, ultra_wide_angle
-    'point_lights': [],             # [[loc1, color1], [loc2, color2],...]
-    'area_light': [(2., 3., 40.),  # location
-                   'White',       # color
-                   .7, .7, 3, 3],  # width, height, Nlamps_x, Nlamps_y
-    'background': 'White',        # color
+    'point_lights': [],  # [[loc1, color1], [loc2, color2],...]
+    'area_light': [
+        (2.0, 3.0, 40.0),  # location
+        'White',  # color
+        0.7,
+        0.7,
+        3,
+        3,
+    ],  # width, height, Nlamps_x, Nlamps_y
+    'background': 'White',  # color
     'textures': None,  # Length of atoms list of texture names
     'celllinewidth': 0.1,  # Radius of the cylinders representing the cell
 }
 
 # Write the .pov (and .ini) file.
 # comment out render not call the povray executable
-renderer = write('NaCl_C6H6.pov', atoms,
-                 **generic_projection_settings,
-                 povray_settings=povray_settings)
+renderer = write(
+    'NaCl_C6H6.pov',
+    atoms,
+    **generic_projection_settings,
+    povray_settings=povray_settings,
+)
 
 renderer.render()
diff -pruN 3.24.0-1/doc/ase/io/write_lammps.py 3.26.0-1/doc/ase/io/write_lammps.py
--- 3.24.0-1/doc/ase/io/write_lammps.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/io/write_lammps.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,6 +1,6 @@
 from ase.io.opls import OPLSff, OPLSStructure
 
 s = OPLSStructure('172_ext.xyz')
-with open("172_defs.par") as fd:
+with open('172_defs.par') as fd:
     opls = OPLSff(fd)
 opls.write_lammps(s, prefix='lmp')
diff -pruN 3.24.0-1/doc/ase/md.rst 3.26.0-1/doc/ase/md.rst
--- 3.24.0-1/doc/ase/md.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/md.rst	2025-08-12 11:26:23.000000000 +0000
@@ -5,15 +5,24 @@ Molecular dynamics
 .. module:: ase.md
    :synopsis: Molecular Dynamics
 
+.. contents::
+
 Typical computer simulations involve moving the atoms around, either
 to optimize a structure (energy minimization) or to do molecular
 dynamics.  This chapter discusses molecular dynamics, energy
-minimization algorithms will be discussed in the :mod:`ase.optimize`
+minimization algorithms is discussed in the :ref:`structure_optimizations`
 section.
 
 A molecular dynamics object will operate on the atoms by moving them
 according to their forces - it integrates Newton's second law
-numerically.  A typical molecular dynamics simulation will use the
+numerically.  In the most basic form, this preserves the particle
+number, volume (actually, the shape of the unit cell), and the energy,
+generating an *NVE* or *microcanonical* ensemble.  Other ensembles
+(*NVT* and *NpT*) can be generated by coupling the motion of the atoms
+to a simple model of the surroundings.
+
+
+*Simple example:* A molecular dynamics simulation in the *NVE* ensemble will use the
 `Velocity Verlet dynamics`_.  You create the
 :class:`ase.md.verlet.VelocityVerlet` object, giving it the atoms and a time
 step, and then you perform dynamics by calling its
@@ -23,9 +32,6 @@ step, and then you perform dynamics by c
                        trajectory='md.traj', logfile='md.log')
   dyn.run(1000)  # take 1000 steps
 
-A number of algorithms can be used to perform molecular
-dynamics, with slightly different results.
-
 
 .. note::
 
@@ -51,7 +57,8 @@ where energy should otherwise be conserv
 
 Experience has shown that 5 femtoseconds is a good choice for most metallic
 systems.  Systems with light atoms (e.g. hydrogen) and/or with strong
-bonds (carbon) will need a smaller time step.
+bonds (carbon) will need a smaller time step, maybe as small as 1 or 2
+fs.
 
 All the dynamics objects documented here are sufficiently related to
 have the same optimal time step.
@@ -163,11 +170,55 @@ Constant NVT simulations (the canonical
 Since Newton's second law conserves energy and not temperature,
 simulations at constant temperature will somehow involve coupling the
 system to a heat bath.  This cannot help being somewhat artificial.
-Two different approaches are possible within ASE.  In Langevin
-dynamics, each atom is coupled to a heat bath through a fluctuating
-force and a friction term.  In Nosé-Hoover dynamics, a term
-representing the heat bath through a single degree of freedom is
-introduced into the Hamiltonian.
+Such algorithms can be stochastic or deterministic, and the coupling
+to the atoms typically occurs through a rescaling of the velocities.
+In the Langevin algorithm, a friction term and a fluctuating force is
+used instead.
+
+In the NVT ensemble both the kinetic energy and the total energy will
+fluctuate.  Some algorithms do not correctly reproduce these
+fluctuations, this may be an issue in particular for small systems,
+and is noted in the overview below.
+
+
+**Recommended algorithms:**
+
+Langevin dynamics
+    A friction and fluctuating force is added to the equation of
+    motion.  *Advantages*: Simple.  Correctly samples the ensemble.
+    *Disadvantage*: Stochastic.
+
+Nosé-Hoover chain
+    In Nosé-Hoover dynamics, the velocities are rescaled at each time
+    step.  The scaling factor is itself a dynamic variable.  For
+    stable operation, a chain of thermostats is used.  *Advantages*:
+    Deterministic.  Well-studied by the Stat. Mek. community.
+    Correctly samples the ensemble.  *Disadvantages*: The fluctuations
+    tend to show a period given by the thermostat time scale.  Slow to
+    reach the correct temperature if started from a significantly
+    wrong temperature.
+
+Bussi dynamics
+    Rescales the velocities at each time step, using a stochastic
+    algorithm to ensure the correct fluctuations, unlike the closely
+    related Berendsen algorithm.  *Advantages*: Simple.  Correctly
+    samples the ensemble.  *Disadvantage*: Stochastic.
+
+**Not recommended algorithms:**
+
+Andersen dynamics
+    At each time step a fraction of the atoms have their velocity
+    replaced with one drawn from the Maxwell-Boltzmann distribution.
+    *Disadvantage*: Dramatically alters a few atoms instead of gently
+    perturbing them all.  Velocities are artificially decorrelated over a time
+    corresponding to the thermalization time.
+
+Berendsen NVT dynamics
+    Rescale the velocities at each time step, so the kinetic energy
+    exponentially approaches the correct one.  *Advantage*: Very
+    efficient for initializing a system to the correct initial
+    temperature.  *Disadvantage*: Almost completely supresses
+    fluctuations in the total energy.
 
 
 Langevin dynamics
@@ -221,6 +272,39 @@ to zero in the part of the system where
 is located.
 
 
+Nosé-Hoover dynamics
+--------------------
+
+.. module:: ase.md.nose_hoover_chain
+
+.. autoclass:: NoseHooverChainNVT
+
+In Nosé-Hoover dynamics, an extra term is added to the Hamiltonian
+representing the coupling to the heat bath.  From a pragmatic point of
+view one can regard Nosé-Hoover dynamics as adding a friction term to
+Newton's second law, but dynamically changing the friction coefficient
+to move the system towards the desired temperature.  Typically the
+"friction coefficient" will fluctuate around zero.  To give a more
+stable dynamics, the friction coefficient is itself thermalized in the
+same way, leading to a chain of thermostats (a **Nosé-Hoover chain**,
+in ASE per default the chain length is 3).
+
+
+Bussi dynamics
+--------------
+
+.. module:: ase.md.bussi
+
+.. autoclass:: Bussi
+
+The Bussi class implements the Bussi dynamics, where constant
+temperature is imposed by a stochastic velocity rescaling algorithm.
+The thermostat is conceptually very close to the Berendsen thermostat,
+but does sample the canonical ensemble correctly. Given that the thermostat
+is both correct and global, it is advised to use it for production
+runs.
+
+
 Andersen dynamics
 -----------------
 
@@ -230,8 +314,8 @@ Andersen dynamics
 
 The Andersen class implements Andersen dynamics, where constant
 temperature is imposed by stochastic collisions with a heat bath.
-With a (small) probability (`andersen_prob`) the collisions act
-occasionally on velocity components of randomly selected particles
+With a (small) probability (``andersen_prob``) the collisions act
+occasionally on velocity components of randomly selected particles.
 Upon a collision the new velocity is drawn from the
 Maxwell-Boltzmann distribution at the corresponding temperature.
 The system is then integrated numerically at constant energy
@@ -259,20 +343,6 @@ References:
 (Academic Press, London, 1996)
 
 
-Nosé-Hoover dynamics
---------------------
-
-In Nosé-Hoover dynamics, an extra term is added to the Hamiltonian
-representing the coupling to the heat bath.  From a pragmatic point of
-view one can regard Nosé-Hoover dynamics as adding a friction term to
-Newton's second law, but dynamically changing the friction coefficient
-to move the system towards the desired temperature.  Typically the
-"friction coefficient" will fluctuate around zero.
-
-Nosé-Hoover dynamics is not implemented as a separate class, but is a
-special case of NPT dynamics.
-
-
 Berendsen NVT dynamics
 -----------------------
 .. module:: ase.md.nvtberendsen
@@ -292,26 +362,34 @@ the gromacs manual at www.gromacs.org.
   dyn = NVTBerendsen(atoms, 0.1 * units.fs, 300, taut=0.5*1000*units.fs)
 
 
-
 Constant NPT simulations (the isothermal-isobaric ensemble)
 ===========================================================
 
-.. module:: ase.md.npt
+Constant pressure (or for solids, constant stress) is usually obtained
+by adding a barostat to one of the NVT algorithms above.  ASE
+currently lacks a good NPT algorithm.  The following two are available.
 
-.. autoclass:: NPT
+**Algorithms:**
+
+Berendsen NPT dynamics
+    This is a variation of Berendsen NVT dynamics with a barostat
+    added.  The size of the unit cell is rescaled after each time
+    step, so the pressure / stress approaches the desired pressure.
+    It exists in two variations, one where the shape of the unit cell
+    is preserved and one where it is allowed to vary.  *Disadvantage*:
+    Fluctuations in both total energy and pressure are suppressed
+    compared to the correct NPT ensemble.  For large systems, this is
+    not expected to be serious.
+
+
+NPT
+    An implementation of NPT dynamics combining a Nosé-Hoover
+    thermostat with a Parinello-Rahman barostat, according to
+    Melchionna *et al.*, see below.  **Not recommended!**  The
+    dynamics tend to be unstable, especially if started with a
+    temperature or pressure that is different from the desired.  The
+    fluctuations seem to often be wrong.
 
-    .. automethod:: run 
-    .. automethod:: set_stress
-    .. automethod:: set_temperature 
-    .. automethod:: set_mask 
-    .. automethod:: set_fraction_traceless 
-    .. automethod:: get_strain_rate 
-    .. automethod:: set_strain_rate 
-    .. automethod:: get_time 
-    .. automethod:: initialize
-    .. automethod:: get_gibbs_free_energy 
-    .. automethod:: zero_center_of_mass_momentum 
-    .. automethod:: attach 
 
        
 Berendsen NPT dynamics
@@ -324,7 +402,8 @@ In Berendsen NPT simulations the velocit
 temperature. The speed of the scaling is determined by the parameter taut.
 
 The atom positions and the simulation cell are scaled in order to achieve
-the desired pressure.
+the desired pressure.  The time scale of this barostat is given by the
+parameter taup.
 
 This method does not result proper NPT sampling but it usually is
 sufficiently good in practice (with large taut and taup). For discussion see
@@ -338,21 +417,36 @@ the gromacs manual at www.gromacs.org. o
                      taut=100 * units.fs, pressure_au=1.01325 * units.bar,
                      taup=1000 * units.fs, compressibility_au=4.57e-5 / units.bar)
 
-Bussi dynamics
---------------
 
-.. module:: ase.md.bussi
+Nosé-Hoover-Parinello-Rahman NPT dynamics
+-----------------------------------------
 
-.. autoclass:: Bussi
+.. module:: ase.md.npt
+
+.. autoclass:: NPT
+
+    .. automethod:: run
+    .. automethod:: set_stress
+    .. automethod:: set_temperature
+    .. automethod:: set_mask
+    .. automethod:: set_fraction_traceless
+    .. automethod:: get_strain_rate
+    .. automethod:: set_strain_rate
+    .. automethod:: get_time
+    .. automethod:: initialize
+    .. automethod:: get_gibbs_free_energy
+    .. automethod:: zero_center_of_mass_momentum
+    .. automethod:: attach
+
+
+**This module is not recommended!**  There is a strong tendency for
+oscillations in the temperature and/or pressure, unless the starting
+configuration is chosen with great care.
 
-The Bussi class implements the Bussi dynamics, where constant
-temperature is imposed by a stochastic velocity rescaling algorithm.
-The thermostat is conceptually very close to the Berendsen thermostat,
-but does sample the canonical ensemble correctly. Given that the thermostat
-is both correct and global, it is advised to use it for production runs.
 
 Contour Exploration
--------------------
+===================
+
 .. module:: ase.md.contour_exploration
 
 .. autoclass:: ContourExploration
@@ -377,8 +471,8 @@ be used at minima since the contour is a
 
 References:
 
-[1] M. J. Waters and J. M. Rondinelli, `Contour Exploration with Potentiostatic
-Kinematics` ArXiv:2103.08054 (https://arxiv.org/abs/2103.08054)
+[1] M. J. Waters and J. M. Rondinelli, *Contour Exploration with Potentiostatic
+Kinematics* `arXiv:2103.08054 <https://arxiv.org/abs/2103.08054>`_.
 
 
 
diff -pruN 3.24.0-1/doc/ase/phasediagram/cuau.py 3.26.0-1/doc/ase/phasediagram/cuau.py
--- 3.24.0-1/doc/ase/phasediagram/cuau.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/phasediagram/cuau.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,11 +3,13 @@ import matplotlib.pyplot as plt
 
 from ase.phasediagram import PhaseDiagram
 
-refs = [('Cu', 0.0),
-        ('Au', 0.0),
-        ('CuAu2', -0.2),
-        ('CuAu', -0.5),
-        ('Cu2Au', -0.7)]
+refs = [
+    ('Cu', 0.0),
+    ('Au', 0.0),
+    ('CuAu2', -0.2),
+    ('CuAu', -0.5),
+    ('Cu2Au', -0.7),
+]
 pd = PhaseDiagram(refs)
 pd.plot(show=False)
 plt.savefig('cuau.png')
diff -pruN 3.24.0-1/doc/ase/phasediagram/ktao.py 3.26.0-1/doc/ase/phasediagram/ktao.py
--- 3.24.0-1/doc/ase/phasediagram/ktao.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/phasediagram/ktao.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,12 +3,20 @@ import matplotlib.pyplot as plt
 
 from ase.phasediagram import PhaseDiagram
 
-references = [('K', 0), ('Ta', 0), ('O2', 0),
-              ('K3TaO8', -16.167), ('KO2', -2.288),
-              ('KO3', -2.239), ('Ta2O5', -19.801),
-              ('TaO3', -8.556), ('TaO', -1.967),
-              ('K2O', -3.076), ('K2O2', -4.257),
-              ('KTaO3', -13.439)]
+references = [
+    ('K', 0),
+    ('Ta', 0),
+    ('O2', 0),
+    ('K3TaO8', -16.167),
+    ('KO2', -2.288),
+    ('KO3', -2.239),
+    ('Ta2O5', -19.801),
+    ('TaO3', -8.556),
+    ('TaO', -1.967),
+    ('K2O', -3.076),
+    ('K2O2', -4.257),
+    ('KTaO3', -13.439),
+]
 pd = PhaseDiagram(references)
 for d in [2, 3]:
     pd.plot(dims=d, show=False)
diff -pruN 3.24.0-1/doc/ase/phasediagram/zno.py 3.26.0-1/doc/ase/phasediagram/zno.py
--- 3.24.0-1/doc/ase/phasediagram/zno.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/phasediagram/zno.py	2025-08-12 11:26:23.000000000 +0000
@@ -14,8 +14,4 @@ pbx.get_pourbaix_energy(0.0, 10.0, verbo
 
 diagram = pbx.diagram(U=np.linspace(-2, 2, 100), pH=np.linspace(0, 14, 100))
 
-diagram.plot(
-    show=False,
-    include_text=True,
-    filename='zno.png'
-)
+diagram.plot(show=False, include_text=True, filename='zno.png')
diff -pruN 3.24.0-1/doc/ase/phonons_Al_fcc.py 3.26.0-1/doc/ase/phonons_Al_fcc.py
--- 3.24.0-1/doc/ase/phonons_Al_fcc.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/phonons_Al_fcc.py	2025-08-12 11:26:23.000000000 +0000
@@ -24,19 +24,25 @@ dos = ph.get_dos(kpts=(20, 20, 20)).samp
 import matplotlib.pyplot as plt  # noqa
 
 fig = plt.figure(figsize=(7, 4))
-ax = fig.add_axes([.12, .07, .67, .85])
+ax = fig.add_axes([0.12, 0.07, 0.67, 0.85])
 
 emax = 0.035
 bs.plot(ax=ax, emin=0.0, emax=emax)
 
-dosax = fig.add_axes([.8, .07, .17, .85])
-dosax.fill_between(dos.get_weights(), dos.get_energies(), y2=0, color='grey',
-                   edgecolor='k', lw=1)
+dosax = fig.add_axes([0.8, 0.07, 0.17, 0.85])
+dosax.fill_between(
+    dos.get_weights(),
+    dos.get_energies(),
+    y2=0,
+    color='grey',
+    edgecolor='k',
+    lw=1,
+)
 
 dosax.set_ylim(0, emax)
 dosax.set_yticks([])
 dosax.set_xticks([])
-dosax.set_xlabel("DOS", fontsize=18)
+dosax.set_xlabel('DOS', fontsize=18)
 
 fig.savefig('Al_phonon.png')
 
@@ -46,8 +52,9 @@ fig.savefig('Al_phonon.png')
 
 # Write modes for specific q-vector to trajectory files:
 L = path.special_points['L']
-ph.write_modes([l / 2 for l in L], branches=[2], repeat=(8, 8, 8), kT=3e-4,
-               center=True)
+ph.write_modes(
+    [l / 2 for l in L], branches=[2], repeat=(8, 8, 8), kT=3e-4, center=True
+)
 
 
 # Generate gif animation:
diff -pruN 3.24.0-1/doc/ase/spacegroup/spacegroup-cosb3.py 3.26.0-1/doc/ase/spacegroup/spacegroup-cosb3.py
--- 3.24.0-1/doc/ase/spacegroup/spacegroup-cosb3.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/spacegroup/spacegroup-cosb3.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,10 +3,12 @@ from ase.build import cut
 from ase.spacegroup import crystal
 
 a = 9.04
-skutterudite = crystal(('Co', 'Sb'),
-                       basis=[(0.25, 0.25, 0.25), (0.0, 0.335, 0.158)],
-                       spacegroup=204,
-                       cellpar=[a, a, a, 90, 90, 90])
+skutterudite = crystal(
+    ('Co', 'Sb'),
+    basis=[(0.25, 0.25, 0.25), (0.0, 0.335, 0.158)],
+    spacegroup=204,
+    cellpar=[a, a, a, 90, 90, 90],
+)
 
 # Create a new atoms instance with Co at origo including all atoms on the
 # surface of the unit cell
@@ -17,21 +19,26 @@ bondatoms = []
 symbols = cosb3.get_chemical_symbols()
 for i in range(len(cosb3)):
     for j in range(i):
-        if (symbols[i] == symbols[j] == 'Co' and
-                cosb3.get_distance(i, j) < 4.53):
+        if symbols[i] == symbols[j] == 'Co' and cosb3.get_distance(i, j) < 4.53:
             bondatoms.append((i, j))
-        elif (symbols[i] == symbols[j] == 'Sb' and
-              cosb3.get_distance(i, j) < 2.99):
+        elif (
+            symbols[i] == symbols[j] == 'Sb' and cosb3.get_distance(i, j) < 2.99
+        ):
             bondatoms.append((i, j))
 
 # Create nice-looking image using povray
-renderer = io.write('spacegroup-cosb3.pov', cosb3,
-                    rotation='90y',
-                    radii=0.4,
-                    povray_settings=dict(transparent=False,
-                                         camera_type='perspective',
-                                         canvas_width=320,
-                                         bondlinewidth=0.07,
-                                         bondatoms=bondatoms))
+renderer = io.write(
+    'spacegroup-cosb3.pov',
+    cosb3,
+    rotation='90y',
+    radii=0.4,
+    povray_settings=dict(
+        transparent=False,
+        camera_type='perspective',
+        canvas_width=320,
+        bondlinewidth=0.07,
+        bondatoms=bondatoms,
+    ),
+)
 
 renderer.render()
diff -pruN 3.24.0-1/doc/ase/spacegroup/spacegroup-diamond.py 3.26.0-1/doc/ase/spacegroup/spacegroup-diamond.py
--- 3.24.0-1/doc/ase/spacegroup/spacegroup-diamond.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/spacegroup/spacegroup-diamond.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,5 +1,6 @@
 from ase.spacegroup import crystal
 
 a = 3.57
-diamond = crystal('C', [(0, 0, 0)], spacegroup=227,
-                  cellpar=[a, a, a, 90, 90, 90])
+diamond = crystal(
+    'C', [(0, 0, 0)], spacegroup=227, cellpar=[a, a, a, 90, 90, 90]
+)
diff -pruN 3.24.0-1/doc/ase/spacegroup/spacegroup-mg.py 3.26.0-1/doc/ase/spacegroup/spacegroup-mg.py
--- 3.24.0-1/doc/ase/spacegroup/spacegroup-mg.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/spacegroup/spacegroup-mg.py	2025-08-12 11:26:23.000000000 +0000
@@ -2,5 +2,9 @@ from ase.spacegroup import crystal
 
 a = 3.21
 c = 5.21
-mg = crystal('Mg', [(1. / 3., 2. / 3., 3. / 4.)], spacegroup=194,
-             cellpar=[a, a, c, 90, 90, 120])
+mg = crystal(
+    'Mg',
+    [(1.0 / 3.0, 2.0 / 3.0, 3.0 / 4.0)],
+    spacegroup=194,
+    cellpar=[a, a, c, 90, 90, 120],
+)
diff -pruN 3.24.0-1/doc/ase/spacegroup/spacegroup-nacl.py 3.26.0-1/doc/ase/spacegroup/spacegroup-nacl.py
--- 3.24.0-1/doc/ase/spacegroup/spacegroup-nacl.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/spacegroup/spacegroup-nacl.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,5 +1,9 @@
 from ase.spacegroup import crystal
 
 a = 5.64
-nacl = crystal(['Na', 'Cl'], [(0, 0, 0), (0.5, 0.5, 0.5)], spacegroup=225,
-               cellpar=[a, a, a, 90, 90, 90])
+nacl = crystal(
+    ['Na', 'Cl'],
+    [(0, 0, 0), (0.5, 0.5, 0.5)],
+    spacegroup=225,
+    cellpar=[a, a, a, 90, 90, 90],
+)
diff -pruN 3.24.0-1/doc/ase/spacegroup/spacegroup-rutile.py 3.26.0-1/doc/ase/spacegroup/spacegroup-rutile.py
--- 3.24.0-1/doc/ase/spacegroup/spacegroup-rutile.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/spacegroup/spacegroup-rutile.py	2025-08-12 11:26:23.000000000 +0000
@@ -2,5 +2,9 @@ from ase.spacegroup import crystal
 
 a = 4.6
 c = 2.95
-rutile = crystal(['Ti', 'O'], basis=[(0, 0, 0), (0.3, 0.3, 0.0)],
-                 spacegroup=136, cellpar=[a, a, c, 90, 90, 90])
+rutile = crystal(
+    ['Ti', 'O'],
+    basis=[(0, 0, 0), (0.3, 0.3, 0.0)],
+    spacegroup=136,
+    cellpar=[a, a, c, 90, 90, 90],
+)
diff -pruN 3.24.0-1/doc/ase/spacegroup/spacegroup-skutterudite.py 3.26.0-1/doc/ase/spacegroup/spacegroup-skutterudite.py
--- 3.24.0-1/doc/ase/spacegroup/spacegroup-skutterudite.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/spacegroup/spacegroup-skutterudite.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,7 +1,9 @@
 from ase.spacegroup import crystal
 
 a = 9.04
-skutterudite = crystal(('Co', 'Sb'),
-                       basis=[(0.25, 0.25, 0.25), (0.0, 0.335, 0.158)],
-                       spacegroup=204,
-                       cellpar=[a, a, a, 90, 90, 90])
+skutterudite = crystal(
+    ('Co', 'Sb'),
+    basis=[(0.25, 0.25, 0.25), (0.0, 0.335, 0.158)],
+    spacegroup=204,
+    cellpar=[a, a, a, 90, 90, 90],
+)
diff -pruN 3.24.0-1/doc/ase/spacegroup/spacegroup.py 3.26.0-1/doc/ase/spacegroup/spacegroup.py
--- 3.24.0-1/doc/ase/spacegroup/spacegroup.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/spacegroup/spacegroup.py	2025-08-12 11:26:23.000000000 +0000
@@ -8,14 +8,17 @@ for name in ['al', 'mg', 'fe', 'diamond'
     py = f'spacegroup-{name}.py'
     dct = runpy.run_path(py)
     atoms = dct[name]
-    renderer = ase.io.write('spacegroup-%s.pov' % name,
-                            atoms,
-                            rotation='10x,-10y',
-                            povray_settings=dict(
-                                transparent=False,
-                                # canvas_width=128,
-                                # celllinewidth=0.02,
-                                celllinewidth=0.05))
+    renderer = ase.io.write(
+        'spacegroup-%s.pov' % name,
+        atoms,
+        rotation='10x,-10y',
+        povray_settings=dict(
+            transparent=False,
+            # canvas_width=128,
+            # celllinewidth=0.02,
+            celllinewidth=0.05,
+        ),
+    )
     renderer.render()
 
 runpy.run_path('spacegroup-cosb3.py')
diff -pruN 3.24.0-1/doc/ase/thermochemistry/ethane.py 3.26.0-1/doc/ase/thermochemistry/ethane.py
--- 3.24.0-1/doc/ase/thermochemistry/ethane.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/thermochemistry/ethane.py	2025-08-12 11:26:23.000000000 +0000
@@ -2,46 +2,52 @@ from numpy import array
 
 from ase.thermochemistry import HinderedThermo
 
-vibs = array([3049.060670,
-              3040.796863,
-              3001.661338,
-              2997.961647,
-              2866.153162,
-              2750.855460,
-              1436.792655,
-              1431.413595,
-              1415.952186,
-              1395.726300,
-              1358.412432,
-              1335.922737,
-              1167.009954,
-              1142.126116,
-              1013.918680,
-              803.400098,
-              783.026031,
-              310.448278,
-              136.112935,
-              112.939853,
-              103.926392,
-              77.262869,
-              60.278004,
-              25.825447])
+vibs = array(
+    [
+        3049.060670,
+        3040.796863,
+        3001.661338,
+        2997.961647,
+        2866.153162,
+        2750.855460,
+        1436.792655,
+        1431.413595,
+        1415.952186,
+        1395.726300,
+        1358.412432,
+        1335.922737,
+        1167.009954,
+        1142.126116,
+        1013.918680,
+        803.400098,
+        783.026031,
+        310.448278,
+        136.112935,
+        112.939853,
+        103.926392,
+        77.262869,
+        60.278004,
+        25.825447,
+    ]
+)
 vib_energies = vibs / 8065.54429  # convert to eV from cm^-1
-trans_barrier_energy = 0.049313   # eV
-rot_barrier_energy = 0.017675     # eV
-sitedensity = 1.5e15              # cm^-2
+trans_barrier_energy = 0.049313  # eV
+rot_barrier_energy = 0.017675  # eV
+sitedensity = 1.5e15  # cm^-2
 rotationalminima = 6
 symmetrynumber = 1
-mass = 30.07                      # amu
-inertia = 73.149                  # amu Ang^-2
+mass = 30.07  # amu
+inertia = 73.149  # amu Ang^-2
 
-thermo = HinderedThermo(vib_energies=vib_energies,
-                        trans_barrier_energy=trans_barrier_energy,
-                        rot_barrier_energy=rot_barrier_energy,
-                        sitedensity=sitedensity,
-                        rotationalminima=rotationalminima,
-                        symmetrynumber=symmetrynumber,
-                        mass=mass,
-                        inertia=inertia)
+thermo = HinderedThermo(
+    vib_energies=vib_energies,
+    trans_barrier_energy=trans_barrier_energy,
+    rot_barrier_energy=rot_barrier_energy,
+    sitedensity=sitedensity,
+    rotationalminima=rotationalminima,
+    symmetrynumber=symmetrynumber,
+    mass=mass,
+    inertia=inertia,
+)
 
 F = thermo.get_helmholtz_energy(temperature=298.15)
diff -pruN 3.24.0-1/doc/ase/thermochemistry/gold.py 3.26.0-1/doc/ase/thermochemistry/gold.py
--- 3.24.0-1/doc/ase/thermochemistry/gold.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/thermochemistry/gold.py	2025-08-12 11:26:23.000000000 +0000
@@ -6,10 +6,13 @@ from ase.thermochemistry import CrystalT
 
 # Set up gold bulk and attach EMT calculator
 a = 4.078
-atoms = crystal('Au', (0., 0., 0.),
-                spacegroup=225,
-                cellpar=[a, a, a, 90, 90, 90],
-                pbc=(1, 1, 1))
+atoms = crystal(
+    'Au',
+    (0.0, 0.0, 0.0),
+    spacegroup=225,
+    cellpar=[a, a, a, 90, 90, 90],
+    pbc=(1, 1, 1),
+)
 calc = EMT()
 atoms.calc = calc
 qn = QuasiNewton(atoms)
@@ -19,14 +22,21 @@ potentialenergy = atoms.get_potential_en
 # Phonon analysis
 N = 5
 ph = Phonons(atoms, calc, supercell=(N, N, N), delta=0.05)
-ph.run()
-ph.read(acoustic=True)
-phonon_energies, phonon_DOS = ph.dos(kpts=(40, 40, 40), npts=3000,
-                                     delta=5e-4)
+try:
+    ph.run()
+    ph.read(acoustic=True)
+finally:
+    ph.clean()
+dos = ph.get_dos(kpts=(40, 40, 40)).sample_grid(npts=3000, width=5e-4, xmin=0.0)
+phonon_energies = dos.get_energies()
+phonon_DOS = dos.get_weights()
+
 
 # Calculate the Helmholtz free energy
-thermo = CrystalThermo(phonon_energies=phonon_energies,
-                       phonon_DOS=phonon_DOS,
-                       potentialenergy=potentialenergy,
-                       formula_units=4)
+thermo = CrystalThermo(
+    phonon_energies=phonon_energies,
+    phonon_DOS=phonon_DOS,
+    potentialenergy=potentialenergy,
+    formula_units=4,
+)
 F = thermo.get_helmholtz_energy(temperature=298.15)
diff -pruN 3.24.0-1/doc/ase/thermochemistry/nitrogen.py 3.26.0-1/doc/ase/thermochemistry/nitrogen.py
--- 3.24.0-1/doc/ase/thermochemistry/nitrogen.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/thermochemistry/nitrogen.py	2025-08-12 11:26:23.000000000 +0000
@@ -14,9 +14,12 @@ vib = Vibrations(atoms)
 vib.run()
 vib_energies = vib.get_energies()
 
-thermo = IdealGasThermo(vib_energies=vib_energies,
-                        potentialenergy=potentialenergy,
-                        atoms=atoms,
-                        geometry='linear',
-                        symmetrynumber=2, spin=0)
-G = thermo.get_gibbs_energy(temperature=298.15, pressure=101325.)
+thermo = IdealGasThermo(
+    vib_energies=vib_energies,
+    potentialenergy=potentialenergy,
+    atoms=atoms,
+    geometry='linear',
+    symmetrynumber=2,
+    spin=0,
+)
+G = thermo.get_gibbs_energy(temperature=298.15, pressure=101325.0)
diff -pruN 3.24.0-1/doc/ase/thermochemistry/thermochemistry.py 3.26.0-1/doc/ase/thermochemistry/thermochemistry.py
--- 3.24.0-1/doc/ase/thermochemistry/thermochemistry.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/thermochemistry/thermochemistry.py	2025-08-12 11:26:23.000000000 +0000
@@ -10,23 +10,23 @@ def run_script_and_get_output(script):
     as a string."""
     script = os.path.join(os.getcwd(), script)
     with tempfile.TemporaryDirectory() as tempdir:
-        return subprocess.check_output([sys.executable, script],
-                                       cwd=tempdir,
-                                       encoding='utf8')
+        return subprocess.check_output(
+            [sys.executable, script], cwd=tempdir, encoding='utf8'
+        )
 
 
 # Only save the parts relevant to thermochemistry
 output = run_script_and_get_output('nitrogen.py')
-output = output[output.find('Enthalpy'):]
+output = output[output.find('Enthalpy') :]
 with open('nitrogen.txt', 'w') as f:
     f.write(output)
 
 output = run_script_and_get_output('ethane.py')
-output = output[output.find('Internal'):]
+output = output[output.find('Internal') :]
 with open('ethane.txt', 'w') as f:
     f.write(output)
 
 output = run_script_and_get_output('gold.py')
-output = output[output.find('Internal'):]
+output = output[output.find('Internal') :]
 with open('gold.txt', 'w') as f:
     f.write(output)
diff -pruN 3.24.0-1/doc/ase/transport/transport_setup.py 3.26.0-1/doc/ase/transport/transport_setup.py
--- 3.24.0-1/doc/ase/transport/transport_setup.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/transport/transport_setup.py	2025-08-12 11:26:23.000000000 +0000
@@ -8,27 +8,35 @@ from ase.io import write
 
 a = 3.92  # Experimental lattice constant
 sqrt = np.sqrt
-cell = np.array([[a / sqrt(3), 0., 0.],
-                 [0., a / sqrt(2), 0.],
-                 [0., a / sqrt(8), a * sqrt(3 / 8.)]])
+cell = np.array(
+    [
+        [a / sqrt(3), 0.0, 0.0],
+        [0.0, a / sqrt(2), 0.0],
+        [0.0, a / sqrt(8), a * sqrt(3 / 8.0)],
+    ]
+)
 repeat = (1, 3, 3)
 
-A = Atoms('Pt', pbc=True, positions=[(0., 0., 0.)], cell=[1, 1, 1])
-B = Atoms('Pt', pbc=True, positions=[(0., 1 / 3., 1 / 3.)], cell=[1, 1, 1])
-C = Atoms('Pt', pbc=True, positions=[(0., 2 / 3., 2 / 3.)], cell=[1, 1, 1])
+A = Atoms('Pt', pbc=True, positions=[(0.0, 0.0, 0.0)], cell=[1, 1, 1])
+B = Atoms('Pt', pbc=True, positions=[(0.0, 1 / 3.0, 1 / 3.0)], cell=[1, 1, 1])
+C = Atoms('Pt', pbc=True, positions=[(0.0, 2 / 3.0, 2 / 3.0)], cell=[1, 1, 1])
 
 A *= repeat
 B *= repeat
 C *= repeat
 
-pyramid_BC = Atoms('Pt4',
-                   pbc=True,
-                   tags=[1, 1, 1, 2],
-                   positions=[(0., 1 / 3., 1 / 3.),  # B
-                              (0., 4 / 3., 1 / 3.),  # B
-                              (0., 1 / 3., 4 / 3.),  # B
-                              (1., 2 / 3., 2 / 3.)],  # C
-                   cell=[1, 1, 1])
+pyramid_BC = Atoms(
+    'Pt4',
+    pbc=True,
+    tags=[1, 1, 1, 2],
+    positions=[
+        (0.0, 1 / 3.0, 1 / 3.0),  # B
+        (0.0, 4 / 3.0, 1 / 3.0),  # B
+        (0.0, 1 / 3.0, 4 / 3.0),  # B
+        (1.0, 2 / 3.0, 2 / 3.0),
+    ],  # C
+    cell=[1, 1, 1],
+)
 
 inv_pyramid_BC = pyramid_BC.copy()
 inv_pyramid_BC.positions[:, 0] *= -1
@@ -41,21 +49,23 @@ def pos(atoms, x):
 
 
 princ = pos(A, 0) + pos(B, 1) + pos(C, 2)
-large = (pos(princ, -8) +
-         pos(princ, -4) +
-         pos(princ, 0) +
-         pos(A, 3) +
-         pos(pyramid_BC, 4) +
-         pos(inv_pyramid_BC, 3) +
-         pos(princ, 4) +
-         pos(princ, 8))
+large = (
+    pos(princ, -8)
+    + pos(princ, -4)
+    + pos(princ, 0)
+    + pos(A, 3)
+    + pos(pyramid_BC, 4)
+    + pos(inv_pyramid_BC, 3)
+    + pos(princ, 4)
+    + pos(princ, 8)
+)
 
 large.set_cell(cell * repeat, scale_atoms=True)
 large.cell[0, 0] = 7 * large.cell[0, 0]
 
-dist = 18.
+dist = 18.0
 large.cell[0, 0] += dist - cell[0, 0]
-large.positions[-(9 * 6 + 4):, 0] += dist - cell[0, 0]
+large.positions[-(9 * 6 + 4) :, 0] += dist - cell[0, 0]
 
 tipL, tipR = large.positions[large.get_tags() == 2]
 tipdist = np.linalg.norm(tipL - tipR)
@@ -65,8 +75,8 @@ mol.rotate('y', 'x')
 mol.rotate('z', 'y')
 
 large += mol
-large.positions[-len(mol):] += tipL
-large.positions[-len(mol):, 0] += tipdist / 2
+large.positions[-len(mol) :] += tipL
+large.positions[-len(mol) :, 0] += tipdist / 2
 
 old = large.cell.copy()
 large *= (1, 1, 3)
@@ -75,12 +85,12 @@ large.set_cell(old)
 # view(large)
 
 colors = np.zeros((len(large), 3))
-colors[:] = [1., 1., .75]
+colors[:] = [1.0, 1.0, 0.75]
 
-pr = [.7, .1, .1]
+pr = [0.7, 0.1, 0.1]
 H = [1, 1, 1]
-C = [.3, .3, .3]
-Pt = [.7, .7, .9]
+C = [0.3, 0.3, 0.3]
+Pt = [0.7, 0.7, 0.9]
 
 colors[164:218] = pr  # principal layer
 colors[289:316] = pr  # principal layer
@@ -91,8 +101,11 @@ colors[322:328] = H  # Molecule H
 
 # write('test.png', large, rotation='-90x,-13y', radii=.9,
 #       show_unit_cell=0, colors=colors)
-write('transport_setup.pov', large,
-      rotation='-90x,-13y', radii=1.06,
-      show_unit_cell=0,
-      povray_settings=dict(colors=colors,
-                           transparent=False)).render()
+write(
+    'transport_setup.pov',
+    large,
+    rotation='-90x,-13y',
+    radii=1.06,
+    show_unit_cell=0,
+    povray_settings=dict(colors=colors, transparent=False),
+).render()
diff -pruN 3.24.0-1/doc/ase/vibrations/H2Morse_calc_overlap.py 3.26.0-1/doc/ase/vibrations/H2Morse_calc_overlap.py
--- 3.24.0-1/doc/ase/vibrations/H2Morse_calc_overlap.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/vibrations/H2Morse_calc_overlap.py	2025-08-12 11:26:23.000000000 +0000
@@ -2,6 +2,7 @@ from ase.calculators.h2morse import H2Mo
 from ase.vibrations.resonant_raman import ResonantRamanCalculator
 
 atoms = H2Morse()
-rmc = ResonantRamanCalculator(atoms, H2MorseExcitedStatesCalculator,
-                              overlap=lambda x, y: x.overlap(y))
+rmc = ResonantRamanCalculator(
+    atoms, H2MorseExcitedStatesCalculator, overlap=lambda x, y: x.overlap(y)
+)
 rmc.run()
diff -pruN 3.24.0-1/doc/ase/vibrations/H2_optical.py 3.26.0-1/doc/ase/vibrations/H2_optical.py
--- 3.24.0-1/doc/ase/vibrations/H2_optical.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/vibrations/H2_optical.py	2025-08-12 11:26:23.000000000 +0000
@@ -9,13 +9,18 @@ atoms = Cluster('relaxed.traj')
 atoms.minimal_box(3.5, h=h)
 
 # relax the molecule
-calc = GPAW(h=h, occupations=FermiDirac(width=0.1),
-            eigensolver='cg', symmetry={'point_group': False},
-            nbands=10, convergence={'eigenstates': 1.e-5,
-                                    'bands': 4})
+calc = GPAW(
+    h=h,
+    occupations=FermiDirac(width=0.1),
+    eigensolver='cg',
+    symmetry={'point_group': False},
+    nbands=10,
+    convergence={'eigenstates': 1.0e-5, 'bands': 4},
+)
 atoms.calc = calc
 
 # use only the 4 converged states for linear response calculation
-rmc = ResonantRamanCalculator(atoms, LrTDDFT,
-                              exkwargs={'restrict': {'jend': 3}})
+rmc = ResonantRamanCalculator(
+    atoms, LrTDDFT, exkwargs={'restrict': {'jend': 3}}
+)
 rmc.run()
diff -pruN 3.24.0-1/doc/ase/vibrations/H2_optical_overlap.py 3.26.0-1/doc/ase/vibrations/H2_optical_overlap.py
--- 3.24.0-1/doc/ase/vibrations/H2_optical_overlap.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/vibrations/H2_optical_overlap.py	2025-08-12 11:26:23.000000000 +0000
@@ -10,14 +10,21 @@ atoms = Cluster('relaxed.traj')
 atoms.minimal_box(3.5, h=h)
 
 # relax the molecule
-calc = GPAW(h=h, occupations=FermiDirac(width=0.1),
-            symmetry={'point_group': False},
-            nbands=10, convergence={'eigenstates': 1.e-5,
-                                    'bands': 4})
+calc = GPAW(
+    h=h,
+    occupations=FermiDirac(width=0.1),
+    symmetry={'point_group': False},
+    nbands=10,
+    convergence={'eigenstates': 1.0e-5, 'bands': 4},
+)
 atoms.calc = calc
 
 # use only the 4 converged states for linear response calculation
-rmc = ResonantRamanCalculator(atoms, LrTDDFT, name='rroverlap',
-                              exkwargs={'restrict': {'jend': 3}},
-                              overlap=lambda x, y: Overlap(x).pseudo(y)[0])
+rmc = ResonantRamanCalculator(
+    atoms,
+    LrTDDFT,
+    name='rroverlap',
+    exkwargs={'restrict': {'jend': 3}},
+    overlap=lambda x, y: Overlap(x).pseudo(y)[0],
+)
 rmc.run()
diff -pruN 3.24.0-1/doc/ase/visualize/matplotlib_plot_atoms.py 3.26.0-1/doc/ase/visualize/matplotlib_plot_atoms.py
--- 3.24.0-1/doc/ase/visualize/matplotlib_plot_atoms.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/visualize/matplotlib_plot_atoms.py	2025-08-12 11:26:23.000000000 +0000
@@ -16,8 +16,9 @@ fig.savefig('matplotlib_plot_atoms1.png'
 slab = FaceCenteredCubic('Au', size=(2, 2, 2))
 fig, axarr = plt.subplots(1, 4, figsize=(15, 5))
 plot_atoms(slab, axarr[0], radii=0.3, rotation=('0x,0y,0z'))
-plot_atoms(slab, axarr[1], scale=0.7, offset=(3, 4), radii=0.3,
-           rotation=('0x,0y,0z'))
+plot_atoms(
+    slab, axarr[1], scale=0.7, offset=(3, 4), radii=0.3, rotation=('0x,0y,0z')
+)
 plot_atoms(slab, axarr[2], radii=0.3, rotation=('45x,45y,0z'))
 plot_atoms(slab, axarr[3], radii=0.3, rotation=('0x,0y,0z'))
 axarr[0].set_title('No rotation')
@@ -30,12 +31,14 @@ fig.savefig('matplotlib_plot_atoms2.png'
 
 stem_image = mpimg.imread('stem_image.jpg')
 atom_pos = [(0.0, 0.0, 0.0), (0.5, 0.5, 0.5), (0.5, 0.5, 0.0)]
-srtio3 = crystal(['Sr', 'Ti', 'O'], atom_pos, spacegroup=221, cellpar=3.905,
-                 size=(3, 3, 3))
+srtio3 = crystal(
+    ['Sr', 'Ti', 'O'], atom_pos, spacegroup=221, cellpar=3.905, size=(3, 3, 3)
+)
 fig, ax = plt.subplots()
 ax.imshow(stem_image, cmap='gray')
-plot_atoms(srtio3, ax, radii=0.3, scale=6.3, offset=(47, 54),
-           rotation=('90x,45y,56z'))
+plot_atoms(
+    srtio3, ax, radii=0.3, scale=6.3, offset=(47, 54), rotation=('90x,45y,56z')
+)
 ax.set_xlim(0, stem_image.shape[0])
 ax.set_ylim(0, stem_image.shape[1])
 ax.set_axis_off()
diff -pruN 3.24.0-1/doc/ase/visualize/mlab_options.py 3.26.0-1/doc/ase/visualize/mlab_options.py
--- 3.24.0-1/doc/ase/visualize/mlab_options.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/visualize/mlab_options.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,5 +1,6 @@
 # creates: mlab_options.txt
 import subprocess
 
-subprocess.check_call('python3 -m ase.visualize.mlab -h > mlab_options.txt',
-                      shell=True)
+subprocess.check_call(
+    'python3 -m ase.visualize.mlab -h > mlab_options.txt', shell=True
+)
diff -pruN 3.24.0-1/doc/ase/xrdebye.py 3.26.0-1/doc/ase/xrdebye.py
--- 3.24.0-1/doc/ase/xrdebye.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ase/xrdebye.py	2025-08-12 11:26:23.000000000 +0000
@@ -6,8 +6,9 @@ from ase.cluster.cubic import FaceCenter
 from ase.utils.xrdebye import XrDebye
 
 # create nanoparticle with approx. 2 nm diameter
-atoms = FaceCenteredCubic('Ag', [(1, 0, 0), (1, 1, 0), (1, 1, 1)],
-                          [6, 8, 8], 4.09)
+atoms = FaceCenteredCubic(
+    'Ag', [(1, 0, 0), (1, 1, 0), (1, 1, 1)], [6, 8, 8], 4.09
+)
 # setup for desired wavelength
 xrd = XrDebye(atoms=atoms, wavelength=0.50523)
 # calculate and plot diffraction pattern
diff -pruN 3.24.0-1/doc/changelog.rst 3.26.0-1/doc/changelog.rst
--- 3.24.0-1/doc/changelog.rst	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/doc/changelog.rst	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,408 @@
+.. _changelog:
+
+=========
+Changelog
+=========
+
+Git master branch
+=================
+
+.. CHANGELOG HOWTO.
+
+   To add an entry to the changelog, create a file named
+   <timestamp>_<subject>.rst inside the ase/changelog.d/ directory.
+   Timestamp should be at least YYYYMMDD.
+
+   You can also install scriv (https://pypi.org/project/scriv/) and run
+   "scriv create" to do this automatically, if you do this often.
+
+   Edit the file following a similar style to other changelog entries and
+   try to choose an existing section for the release note.
+
+   For example ase/changelog.d/20250108_amber_fix_velocities.rst with contents:
+
+     Calculators
+     -----------
+
+     - Amber: Fix scaling of velocities in restart files (:mr:`3427`)
+
+   For each release we generate a full changelog which is inserted below.
+
+.. scriv-auto-changelog-start
+
+Version 3.26.0
+==============
+
+I/O
+---
+
+- Added communicator argument to parprint, which defaults to world if None, analogous as for paropen
+
+- Added single float encoding for :mod:`~ase.io.jsonio` (:mr:`3682`)
+
+- Changed :func:`~ase.io.extxyz.write_extxyz` to store
+  :class:`~ase.constraints.FixAtoms` and
+  :class:`~ase.constraints.FixCartesian` by default without explicitly
+  specifying ``move_mask`` in ``columns`` (:mr:`3713`)
+
+- **Breaking change**: Removed IOFormat.open() method. It is untested and appears to be unused. :mr:`3738`
+
+- Fix :func:`~ase.io.vasp.read_vasp` to correctly read both atomic and lattice velocities if present in POSCAR (:mr:`3762`)
+
+Calculators
+-----------
+
+- Added per-atom ``energies`` consistent with LAMMPS to
+  :class:`~ase.calculators.tersoff.Tersoff` (:mr:`3656`)
+
+- Added toggles between analytical and numerical forces/stress in
+  :class:`~ase.calculators.fd.FiniteDifferenceCalculator` (:mr:`3678`)
+
+- Added calculators ``mattersim`` and ``mace_mp`` to the ``get_calculator()`` function
+
+- Changed :class:`~ase.calculators.elk.ELK` based on
+  :class:`~ase.calculators.GenericFileIOCalculator` (:mr:`3736`)
+
+- DFTD3 no longer warns about systems that are neither 3D periodic
+  nor 0D, because there is no way to adapt the code that resolves the
+  condition warned about.  (:mr:`3740`)
+
+Optimizers
+----------
+
+ - Logfile and trajectory inputs now accept both string and Path objects.
+
+- **Breaking change:** The :class:`~ase.utils.abc.Optimizable` interface
+  now works in terms of arbitrary degrees of freedom rather than
+  Cartesian (Nx3) ones.
+  Please note that the interface is still considered an internal feature
+  and may still change significantly. (:mr:`3732`)
+
+Molecular dynamics
+------------------
+
+- Added anisotropic NpT with MTK equations (:mr:`3595`).
+
+- Fixed bug in Nose-Hoover chain thermostat which would inconsistently update extended variables for the thermostat.
+
+GUI
+---
+
+ - Atomic spins can now be visualized as arrows
+
+- Mouse button 2 and 3 are now equivalent in the GUI, which simplifies
+  life on particularly MacOS (:mr:`3669`).
+- Menu shortcut keys now work as expected on MacOS.
+- In Rotate and Translate mode, Ctrl + arrow key now works as intended on
+  MacOS.  Left alt and Command now have the same effect (:mr:`3669`).
+
+- Changed Alt+X, Alt+Y, Alt+Z to Shift+X, Shift+Y, Shift+Z to view planes from "other side"
+- Changed views into basis vector planes to I, J, K, Shift+I, Shift+J, Shift+K
+
+- Added general window to view and edit data on atoms directly
+  in the same style as the cell editor.
+  The window currently edits
+  symbols and Cartesian positions only (:mr:`3790`).
+
+Development
+-----------
+
+ - Enable ruff for whole documentation
+
+Documentation
+-------------
+
+ - Web page now uses sphinx book theme (:mr:`3684`).
+
+- Documentation moved to `ase-lib.org <https://ase-lib.org/>`_.
+
+Other changes
+-------------
+
+- Removed `Quaternions` (subclass of `Atoms`).
+  The `quaternions` read from a LAMMPS data file is still accessible as an array
+  in `Atoms`. (:mr:`3709`)
+
+- Re-added the ``spin`` option of
+  :meth:`~ase.spectrum.band_structure.BandStructurePlot.plot`
+  to plot only the specified spin channel (:mr:`3726`)
+
+Bugfixes
+--------
+
+- Fixed :class:`~ase.calculators.tersoff.Tersoff` to compute properties
+  correctly (:mr:`3653`, :mr:`3655`, :mr:`3657`).
+
+- Enable :func:`ase.io.magres.read_magres` to handle cases from CASTEP < 23 where indices and labels are "munged" together if the index exceeds 99. If an index exceeds 999 the situation remains ambiguous and an error will be raised. (:mr:`3530`)
+
+- Fix duplicated transformation (e.g. rotation) of symmetry labels in :func:`~ase.dft.bz.bz_plot` (:mr:`3617`).
+
+- Fixed bug in :class:`io.utils.PlottingVariables` where automatic
+  bounding boxes were incorrectly centered due the image center not being
+  scaled for paper space (:mr:`3769`).
+
+- Fixed bug in :class:`io.pov.POVRAY` where unspecified image (canvas)
+  dimensions would use defaults with an incorrect aspect ratio (:mr:`3769`).
+
+Structure tools
+---------------
+
+- Added ``score_key='metric'`` to :func:`~ase.build.find_optimal_cell_shape`
+  for scoring a cell based on its metric tensor (:mr:`3616`)
+
+Version 3.25.0
+==============
+
+I/O
+---
+
+- Moved Postgres, MariaDB and MySQL backends to separate project:
+  https://gitlab.com/ase/ase-db-backends.  Install from PyPI with
+  ``pip install ase-db-backends`` (:mr:`3545`).
+
+- **BREAKING** ase.io.orca `read_orca_output` now returns Atoms with attached properties.
+  `ase.io.read` will use this function.
+  The previous behaviour (return results dictionary only) is still available from function `read_orca_outputs`. (:mr:`3599`)
+
+- Added :func:`~ase.io.castep.write_castep_geom` and
+  :func:`~ase.io.castep.write_castep_md` (:mr:`3229`)
+
+- Fixed `:mod:`ase.data.pubchem` module to convert ``#`` in SMILES to HEX
+  ``%23`` for URL (:mr:`3620`).
+
+ - :mod:`ase.db`: Unique IDs are now based on UUID rather than pseudorandom numbers that could become equal due to seeding (:mr:`3614`).
+ - :mod:`ase.db`: Fix bug where unique_id could be converted to float or int (:mr:`3613`).
+ - Vasp: More robust reading of CHGCAR (:mr:`3607`).
+ - Lammpsdump: Read timestep from lammpsdump and set element based on mass (:mr:`3529`).
+ - Vasp: Read and write velocities (:mr:`3597`).
+ - DB: Support for LMDB via `ase-db-backends` project (:mr:`3564`, :mr:`3639`).
+ - Espresso: Fix bug reading `alat` in some cases (:mr:`3562`).
+ - GPAW: Fix reading of total charge from text file (:mr:`3519`).
+ - extxyz: Somewhat restrict what properties are automatically written (:mr:`3516`).
+ - Lammpsdump: Read custom property/atom LAMMPS dump data (:mr:`3510`).
+
+Calculators
+-----------
+
+ - More robust reading of Castep XC functional (:mr:`3612`).
+ - More robust saving of calculators to e.g. trajectories (:mr:`3610`).
+ - Lammpslib: Fix outdated MPI check (:mr:`3594`).
+ - Morse: Optionally override neighbor list implementation (:mr:`3593`).
+ - EAM: Calculate stress (:mr:`3581`).
+
+ - A new Calculator :class:`ase.calculators.tersoff.Tersoff` has been added. This is a Python implementation of a LAMMPS-style Tersoff interatomic potential. Parameters may be passed directly to the calculator as a :class:`ase.calculators.tersoff.TersoffParameters` object, or the Calculator may be constructed from a LAMMPS-style file using its ``from_lammps`` classmethod. (:mr:`3502`)
+
+Optimizers
+----------
+
+ - Fix step counting in the
+   :class:`~ase.optimize.cellawarebfgs.CellAwareBFGS` (:mr:`3588`).
+
+ - Slightly more efficient/robust GoodOldQuasiNewton (:mr:`3570`).
+
+Molecular dynamics
+------------------
+
+- Merged `self.communicator` into `self.comm` (:mr:`3631`).
+
+ - Improved random sampling in countour exploration (:mr:`3643`).
+ - Fix small energy error in Langevin dynamics (:mr:`3567`).
+ - Isotropic NPT with MTK equations (:mr:`3550`).
+ - Bussi dynamics now work in parallel (:mr:`3569`).
+ - Improvements to documentation (:mr:`3566`).
+ - Make Nose-Hoover chain NVT faster and fix domain decomposition
+   with Asap3 (:mr:`3571`).
+
+ - NPT now works with cells that are upper or lower triangular matrices
+   (:mr:`3277`) aside from upper-only as before.
+
+ - Fix inconsistent :meth:`irun` for NPT (:mr:`3598`).
+
+GUI
+---
+
+ - Fix windowing bug on WSL (:mr:`3478`).
+
+ - Added button to wrap atoms into cell (:mr:`3587`).
+
+Development
+-----------
+
+- Changelog is now generated using ``scriv`` (:mr:`3572`).
+
+- CI cleanup; pypi dependencies in CI jobs are now cached
+  (:mr:`3628`, :mr:`3629`).
+- Maximum automatic pytest workers reduced to 8 (:mr:`3628`).
+
+ - Ruff formatter to be gradually enabled across codebase (:mr:`3600`).
+
+Other changes
+-------------
+
+ - :meth:`~ase.cell.Cell.standard_form` can convert to upper triangular (:mr:`3623`).
+
+ - Bugfix: :func:`~ase.geometry.geometry.get_duplicate_atoms` now respects pbc (:mr:`3609`).
+
+ - Bugfix: Constraint masks in cell filters are now respected down to numerical precision.  Previously, the constraints could be violated by a small amount (:mr:`3603`).
+ - Deprecate :func:`~ase.utils.lazyproperty` and :func:`~ase.utils.lazymethod`
+   since Python now provides :func:`functools.cached_property` (:mr:`3565`).
+ - Remove `nomad-upload` and `nomad-get` commands due to incompatibility
+   with recent Nomad (:mr:`3563`).
+ - Fix normalization of phonon DOS (:mr:`3472`).
+ - :class:`~ase.io.utils.PlottingVariables` towards rotating the
+   camera rather than the atoms (:mr:`2895`).
+
+.. scriv-auto-changelog-end
+
+
+Version 3.24.0
+==============
+
+Requirements
+------------
+
+* The minimum supported Python version has increased to 3.9 (:mr:`3473`)
+* Support numpy 2 (:mr:`3398`, :mr:`3400`, :mr:`3402`)
+* Support spglib 2.5.0 (:mr:`3452`)
+
+Atoms
+-----
+* New method :func:`~ase.Atoms.get_number_of_degrees_of_freedom()` (:mr:`3380`)
+* New methods :func:`~ase.Atoms.get_kinetic_stress()`, :func:`~ase.Atoms.get_kinetic_stresses()` (:mr:`3362`)
+* Prevent truncation when printing Atoms objects with 1000 or more atoms (:mr:`2518`)
+
+DB
+--
+* Ensure correct float format when writing to Postgres database (:mr:`3475`)
+
+Structure tools
+---------------
+
+* Add atom tagging to ``ase.build.general_surface`` (:mr:`2773`)
+* Fix bug where code could return the wrong lattice when trying to fix the handedness of a 2D lattice  (:mr:`3387`)
+* Major improvements to :func:`~ase.build.find_optimal_cell_shape`: improve target metric; ensure rotationally invariant results; avoid negative determinants; improved performance via vectorisation (:mr:`3404`, :mr:`3441`, :mr:`3474`). The ``norm`` argument to :func:`~ase.build.supercells.get_deviation_from_optimal_cell_shape` is now deprecated.
+* Performance improvements to :class:`ase.spacegroup.spacegroup.Spacegroup` (:mr:`3434`, :mr:`3439`, :mr:`3448`)
+* Deprecated :func:`ase.spacegroup.spacegroup.get_spacegroup` as results can be misleading (:mr:`3455`).
+  
+
+Calculators / IO
+----------------
+
+* Amber: Fix scaling of velocities in restart files (:mr:`3427`)
+* Amber: Raise an error if cell is orthorhombic (:mr:`3443`)
+* CASTEP
+
+  - **BREAKING** Removed legacy ``read_cell`` and ``write_cell`` functions from ase.io.castep. (:mr:`3435`)
+  - .castep file reader bugfix for Windows (:mr:`3379`), testing improved (:mr:`3375`)
+  - fix read from Castep geometry optimisation with stress only (:mr:`3445`)
+
+* EAM: Fix calculations with self.form = "eam" (:mr:`3399`)
+* FHI-aims
+  
+  - make free_energy the default energy (:mr:`3406`)
+  - add legacy DFPT parser hook (:mr:`3495`)
+
+* FileIOSocketClientLauncher: Fix an unintended API change (:mr:`3453`)
+* FiniteDifferenceCalculator: added new calculator which wraps other calculator for finite-difference forces and strains (:mr:`3509`)
+* GenericFileIOCalculator fix interaction with SocketIO (:mr:`3381`)
+* LAMMPS
+
+  - fixed a bug reading dump file with only one atom (:mr:`3423`)
+  - support initial charges (:mr:`2846`, :mr:`3431`)
+
+* MixingCalculator: remove requirement that mixed calculators have common ``implemented_properties`` (:mr:`3480`)
+* MOPAC: Improve version-number parsing (:mr:`3483`)
+* MorsePotential: Add stress (:mr:`3485`)
+* NWChem: fixed reading files from other directories (:mr:`3418`)
+* Octopus: Improved IO testing (:mr:`3465`)
+* ONETEP calculator: allow ``pseudo_path`` to be set in config (:mr:`3385`)
+* Orca: Only parse dipoles if COM is found. (:mr:`3426`)
+* Quantum Espresso
+
+  - allow arbitrary k-point lists (:mr:`3339`)
+  - support keys from EPW (:mr:`3421`)
+  - Fix path handling when running remote calculations from Windows (:mr:`3464`)
+
+* Siesta: support version 5.0 (:mr:`3464`)
+* Turbomole: fixed formatting of "density convergence" parameter (:mr:`3412`)
+* VASP
+
+  - Fixed a bug handling the ICHAIN tag from VTST (:mr:`3415`)
+  - Fixed bugs in CHG file writing (:mr:`3428`) and CHGCAR reading (:mr:`3447`)
+  - Fix parsing POSCAR scale-factor line that includes a comment (:mr:`3487`)
+  - Support use of unknown INCAR keys (:mr:`3488`)
+  - Drop "INCAR created by Atomic Simulation Environment" header (:mr:`3488`)
+  - Drop 1-space indentation of INCAR file (:mr:`3488`)
+  - Use attached atoms if no atom argument provided to :func:`ase.calculators.vasp.Vasp.calculate` (:mr:`3491`)
+
+GUI
+---
+* Refactoring of :class:`ase.gui.view.View` to improve API for external projects (:mr:`3419`)
+* Force lines to appear black (:mr:`3459`)
+* Fix missing Alt+X/Y/Z/1/2/3 shortcuts to set view direction (:mr:`3482`)
+* Fix incorrect frame number after using Page-Up/Page-Down controls (:mr:`3481`)
+* Fix incorrect double application of `repeat` to `energy` in GUI (:mr:`3492`)
+
+Molecular Dynamics
+------------------
+
+* Added Bussi thermostat :class:`ase.md.bussi.Bussi` (:mr:`3350`)
+* Added Nose-Hoover chain NVT thermostat :class:`ase.md.nose_hoover_chain.NoseHooverChainNVT` (:mr:`3508`)
+* Improve ``force_temperature`` to work with constraints (:mr:`3393`)
+* Add ``**kwargs`` to MolecularDynamics, passed to parent Dynamics (:mr:`3403`)
+* Support modern Numpy PRNGs in Andersen thermostat (:mr:`3454`)
+
+Optimizers
+----------
+* **BREAKING** The ``master`` parameter to each Optimizer is now passed via ``**kwargs`` and so becomes keyword-only. (:mr:`3424`)
+* Pass ``comm`` to BFGS and CellAwareBFGS as a step towards cleaner parallelism (:mr:`3397`)
+* **BREAKING** Removed deprecated ``force_consistent`` option from Optimizer (:mr:`3424`)
+
+Phonons
+-------
+
+* Fix scaling of phonon amplitudes (:mr:`3438`)
+* Implement atom-projected PDOS, deprecate :func:`ase.phonons.Phonons.dos` in favour of :func:`ase.phonons.Phonons.get_dos` (:mr:`3460`)
+* Suppress warnings about imaginary frequencies unless :func:`ase.phonons.Phonons.get_dos` is called with new parameter ``verbose=True`` (:mr:`3461`)
+
+Pourbaix (:mr:`3280`)
+---------------------
+
+* New module :mod:`ase.pourbaix` written to replace :class:`ase.phasediagram.Pourbaix`
+* Improved energy definition and diagram generation method
+* Improved visualisation
+
+Spectrum
+--------
+* **BREAKING** :class:`ase.spectrum.band_structure.BandStructurePlot`: the ``plot_with_colors()`` has been removed and its features merged into the ``plot()`` method.
+
+Misc
+----
+* Cleaner bandgap description from :class:`ase.dft.bandgap.GapInfo` (:mr:`3451`)
+
+Documentation
+-------------
+* The "legacy functionality" section has been removed (:mr:`3386`)
+* Other minor improvements and additions (:mr:`2520`, :mr:`3377`, :mr:`3388`, :mr:`3389`, :mr:`3394`, :mr:`3395`, :mr:`3407`, :mr:`3413`, :mr:`3416`, :mr:`3446`, :mr:`3458`, :mr:`3468`)
+
+Testing
+-------
+* Remove some dangling open files (:mr:`3384`)
+* Ensure all test modules are properly packaged (:mr:`3489`)
+
+Units
+-----
+* Added 2022 CODATA values (:mr:`3450`)
+* Fixed value of vacuum magnetic permeability ``_mu0`` in (non-default) CODATA 2018 (:mr:`3486`)
+
+Maintenance and dev-ops
+-----------------------
+* Set up ruff linter (:mr:`3392`, :mr:`3420`)
+* Further linting (:mr:`3396`, :mr:`3425`, :mr:`3430`, :mr:`3433`, :mr:`3469`, :mr:`3520`)
+* Refactoring of ``ase.build.bulk`` (:mr:`3390`), ``ase.spacegroup.spacegroup`` (:mr:`3429`)
+
+Earlier releases
+================
+
+Releases earlier than ASE 3.24.0 do not have separate release notes and changelog.
+Their changes are only listed in the :ref:`releasenotes`.
diff -pruN 3.24.0-1/doc/conf.py 3.26.0-1/doc/conf.py
--- 3.24.0-1/doc/conf.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/conf.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,14 +1,16 @@
 import datetime
 
-extensions = ['ase.utils.sphinx',
-              'sphinx.ext.autodoc',
-              'sphinx.ext.doctest',
-              'sphinx.ext.extlinks',
-              'sphinx.ext.mathjax',
-              'sphinx.ext.viewcode',
-              'sphinx.ext.napoleon',
-              'sphinx.ext.intersphinx',
-              'sphinx.ext.imgconverter']
+extensions = [
+    'ase.utils.sphinx',
+    'sphinx.ext.autodoc',
+    'sphinx.ext.doctest',
+    'sphinx.ext.extlinks',
+    'sphinx.ext.mathjax',
+    'sphinx.ext.viewcode',
+    'sphinx.ext.napoleon',
+    'sphinx.ext.intersphinx',
+    'sphinx.ext.imgconverter',
+]
 
 extlinks = {
     'doi': ('https://doi.org/%s', 'doi: %s'),
@@ -19,36 +21,48 @@ extlinks = {
 source_suffix = '.rst'
 master_doc = 'index'
 project = 'ASE'
+author = 'ASE developers'
 copyright = f'{datetime.date.today().year}, ASE-developers'
-templates_path = ['templates']
 exclude_patterns = ['build']
 default_role = 'math'
 pygments_style = 'sphinx'
 autoclass_content = 'both'
 modindex_common_prefix = ['ase.']
-nitpick_ignore = [('envvar', 'VASP_PP_PATH'),
-                  ('envvar', 'ASE_ABC_COMMAND'),
-                  ('envvar', 'LAMMPS_COMMAND'),
-                  ('envvar', 'ASE_NWCHEM_COMMAND'),
-                  ('envvar', 'SIESTA_COMMAND'),
-                  ('envvar', 'SIESTA_PP_PATH'),
-                  ('envvar', 'VASP_SCRIPT')]
+nitpick_ignore = [
+    ('envvar', 'VASP_PP_PATH'),
+    ('envvar', 'ASE_ABC_COMMAND'),
+    ('envvar', 'LAMMPS_COMMAND'),
+    ('envvar', 'ASE_NWCHEM_COMMAND'),
+    ('envvar', 'SIESTA_COMMAND'),
+    ('envvar', 'SIESTA_PP_PATH'),
+    ('envvar', 'VASP_SCRIPT'),
+]
 
-html_theme = 'sphinx_rtd_theme'
-html_style = 'ase.css'
+html_theme = 'sphinx_book_theme'
+html_logo = 'static/ase256.png'
 html_favicon = 'static/ase.ico'
 html_static_path = ['static']
 html_last_updated_fmt = '%a, %d %b %Y %H:%M:%S'
 
+html_theme_options = {
+    # https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/header-links.html
+    'gitlab_url': 'https://gitlab.com/ase/ase',
+    # https://pydata-sphinx-theme.readthedocs.io/en/stable/user_guide/indices.html
+    'primary_sidebar_end': ['indices.html'],
+}
+
 latex_elements = {'papersize': 'a4paper'}
 latex_show_urls = 'inline'
 latex_show_pagerefs = True
 latex_engine = 'xelatex'
 latex_documents = [
-    ('index', 'ASE.tex', 'ASE', 'ASE-developers', 'howto', not True)]
+    ('index', 'ASE.tex', 'ASE', 'ASE-developers', 'howto', not True)
+]
 
-intersphinx_mapping = {'gpaw': ('https://gpaw.readthedocs.io', None),
-                       'python': ('https://docs.python.org/3.10', None)}
+intersphinx_mapping = {
+    'gpaw': ('https://gpaw.readthedocs.io', None),
+    'python': ('https://docs.python.org/3.10', None),
+}
 
 # Avoid GUI windows during doctest:
 doctest_global_setup = """
diff -pruN 3.24.0-1/doc/development/development.rst 3.26.0-1/doc/development/development.rst
--- 3.24.0-1/doc/development/development.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/development/development.rst	2025-08-12 11:26:23.000000000 +0000
@@ -14,6 +14,7 @@ Development topics:
     contribute
     python_codingstandard
     writing_documentation_ase
+    writing_changelog
     calculators
     making_movies
     newrelease
diff -pruN 3.24.0-1/doc/development/writepngs.py 3.26.0-1/doc/development/writepngs.py
--- 3.24.0-1/doc/development/writepngs.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/development/writepngs.py	2025-08-12 11:26:23.000000000 +0000
@@ -13,5 +13,6 @@ for i, s in enumerate(t):
 
     ofname = str(i) + '.png'
     print('writing', ofname)
-    io.write(ofname, s,
-             bbox=[-3, -5, 50, 22])  # set bbox by hand, try and error
+    io.write(
+        ofname, s, bbox=[-3, -5, 50, 22]
+    )  # set bbox by hand, try and error
diff -pruN 3.24.0-1/doc/development/writing_changelog.rst 3.26.0-1/doc/development/writing_changelog.rst
--- 3.24.0-1/doc/development/writing_changelog.rst	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/doc/development/writing_changelog.rst	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,45 @@
+.. _writing_changelog:
+
+=================
+Writing changelog
+=================
+
+Since ASE 3.24.0 (:mr:`3572`),
+we recommend using |scriv|_ to add your changes in the changelog rather than
+updating ``doc/releasenotes.rst`` directly to avoid merge conflicts on it.
+
+.. |scriv| replace:: ``scriv``
+.. _scriv: https://scriv.readthedocs.io/
+
+Installing ``scriv``
+====================
+
+If you have not installed ``scriv``, you should first install it, e.g., as:
+
+.. code-block:: console
+
+    $ pip install scriv
+
+Using ``scriv``
+===============
+
+Once you have made changes on ASE, you run ``scriv``, e.g., as:
+
+.. code-block:: console
+
+    $ scriv create --add
+
+It makes a file like ``20250101_000000_john_doe_my_change.rst``
+in the ``changelog.d`` directory.
+You can also rename the file under the rule ``<timestamp>_<subject>.rst``,
+where ``<timestamp>`` should be at least ``YYYYMMDD``.
+
+You then uncomment the relevant section, add some notes about the change,
+and commit the updated file.
+
+How does it work?
+=================
+
+When ASE maintainers make a new release, they will compile these files
+automatically using the command ``scriv collect``.
+This will put all changes given in ``changelog.d`` in ``doc/releasenotes.rst``.
diff -pruN 3.24.0-1/doc/development/writing_documentation_ase.rst 3.26.0-1/doc/development/writing_documentation_ase.rst
--- 3.24.0-1/doc/development/writing_documentation_ase.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/development/writing_documentation_ase.rst	2025-08-12 11:26:23.000000000 +0000
@@ -15,11 +15,11 @@ reStructuredText_ markup language.
 Installing Docutils and Sphinx
 ==============================
 
-.. highlight:: bash
+If you do:
 
-If you do::
+.. code-block:: console
 
-    $ pip install sphinx_rtd_theme --user
+    $ pip install sphinx_book_theme --user
 
 and add ``~/.local/bin`` to you :envvar:`PATH` environment variable, then
 you should be ready to go.  You may need the following installed, but they
@@ -38,7 +38,9 @@ reStructuredText_.
 If you don't already have your own copy of the ASE package, then read
 :ref:`here <contribute>` how to get everything set up.
 
-Then :command:`cd` to the :file:`doc` directory and build the html-pages::
+Then :command:`cd` to the :file:`doc` directory and build the HTML pages:
+
+.. code-block:: console
 
   $ cd ~/ase/doc
   $ make
@@ -53,7 +55,9 @@ This might take a long time the first ti
 
 Create a branch for your work, make your changes to the ``.rst`` files, run
 :command:`make` again, check the results and if things
-look ok, create a *merge request*::
+look ok, create a *merge request*:
+
+.. code-block:: console
 
     $ git checkout -b fixdoc
     $ idle index.rst
diff -pruN 3.24.0-1/doc/easybuild/ASE-3.14.1-foss-2016b-Python-2.7.12.eb 3.26.0-1/doc/easybuild/ASE-3.14.1-foss-2016b-Python-2.7.12.eb
--- 3.24.0-1/doc/easybuild/ASE-3.14.1-foss-2016b-Python-2.7.12.eb	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/easybuild/ASE-3.14.1-foss-2016b-Python-2.7.12.eb	2025-08-12 11:26:23.000000000 +0000
@@ -4,7 +4,7 @@ name = 'ASE'
 version = '3.14.1'
 versionsuffix = '-Python-%(pyver)s'
 
-homepage = 'http://wiki.fysik.dtu.dk/ase'
+homepage = 'https://ase-lib.org/'
 description = """ASE is a python package providing an open source Atomic Simulation Environment
  in the Python scripting language."""
 
diff -pruN 3.24.0-1/doc/easybuild/ASE-3.14.1-foss-2016b-Python-3.5.2.eb 3.26.0-1/doc/easybuild/ASE-3.14.1-foss-2016b-Python-3.5.2.eb
--- 3.24.0-1/doc/easybuild/ASE-3.14.1-foss-2016b-Python-3.5.2.eb	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/easybuild/ASE-3.14.1-foss-2016b-Python-3.5.2.eb	2025-08-12 11:26:23.000000000 +0000
@@ -4,7 +4,7 @@ name = 'ASE'
 version = '3.14.1'
 versionsuffix = '-Python-%(pyver)s'
 
-homepage = 'http://wiki.fysik.dtu.dk/ase'
+homepage = 'https://ase-lib.org/'
 description = """ASE is a python package providing an open source Atomic Simulation Environment
  in the Python scripting language."""
 
diff -pruN 3.24.0-1/doc/easybuild/ASE-3.15.0-foss-2016b-Python-2.7.12.eb 3.26.0-1/doc/easybuild/ASE-3.15.0-foss-2016b-Python-2.7.12.eb
--- 3.24.0-1/doc/easybuild/ASE-3.15.0-foss-2016b-Python-2.7.12.eb	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/easybuild/ASE-3.15.0-foss-2016b-Python-2.7.12.eb	2025-08-12 11:26:23.000000000 +0000
@@ -4,7 +4,7 @@ name = 'ASE'
 version = '3.15.0'
 versionsuffix = '-Python-%(pyver)s'
 
-homepage = 'http://wiki.fysik.dtu.dk/ase'
+homepage = 'https://ase-lib.org/'
 description = """ASE is a python package providing an open source Atomic Simulation Environment
  in the Python scripting language."""
 
diff -pruN 3.24.0-1/doc/easybuild/ASE-3.15.0-foss-2016b-Python-3.5.2.eb 3.26.0-1/doc/easybuild/ASE-3.15.0-foss-2016b-Python-3.5.2.eb
--- 3.24.0-1/doc/easybuild/ASE-3.15.0-foss-2016b-Python-3.5.2.eb	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/easybuild/ASE-3.15.0-foss-2016b-Python-3.5.2.eb	2025-08-12 11:26:23.000000000 +0000
@@ -4,7 +4,7 @@ name = 'ASE'
 version = '3.15.0'
 versionsuffix = '-Python-%(pyver)s'
 
-homepage = 'http://wiki.fysik.dtu.dk/ase'
+homepage = 'https://ase-lib.org/'
 description = """ASE is a python package providing an open source Atomic Simulation Environment
  in the Python scripting language."""
 
diff -pruN 3.24.0-1/doc/easybuild/ASE-3.15.0-foss-2017b-Python-3.6.2.eb 3.26.0-1/doc/easybuild/ASE-3.15.0-foss-2017b-Python-3.6.2.eb
--- 3.24.0-1/doc/easybuild/ASE-3.15.0-foss-2017b-Python-3.6.2.eb	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/easybuild/ASE-3.15.0-foss-2017b-Python-3.6.2.eb	2025-08-12 11:26:23.000000000 +0000
@@ -4,7 +4,7 @@ name = 'ASE'
 version = '3.15.0'
 versionsuffix = '-Python-%(pyver)s'
 
-homepage = 'http://wiki.fysik.dtu.dk/ase'
+homepage = 'https://ase-lib.org/'
 description = """ASE is a python package providing an open source Atomic Simulation Environment
  in the Python scripting language."""
 
diff -pruN 3.24.0-1/doc/ecosystem.rst 3.26.0-1/doc/ecosystem.rst
--- 3.24.0-1/doc/ecosystem.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/ecosystem.rst	2025-08-12 11:26:23.000000000 +0000
@@ -33,6 +33,12 @@ Listed in alphabetical order, for want o
    with first-principles codes via ASE as well as other Python
    libraries.
 
+ * `DockOnSurf <https://dockonsurf.readthedocs.io/>`_:
+   DockOnSurf is a program to automatically find the most stable geometry
+   for molecules on surfaces. In a two-step screening procedure, different 
+   configurations are scanned by combining different sites, anchoring points,
+   conformers, orientations and probing dissociation of acidic protons.
+
  * `CHGNet <https://github.com/CederGroupHub/chgnet>`_:
    A pretrained universal neural network potential for charge-informed
    atomistic modeling
@@ -111,6 +117,12 @@ Listed in alphabetical order, for want o
    A flexible platform for high-throughput, database-driven computational 
    materials science and quantum chemistry workflows built around ASE.
 
+ * `RAFFLE <https://github.com/ExeQuantCode/RAFFLE>`_:
+   Iterative local atomic descriptor based interface prediction.
+   RAFFLE iteratively generates and evaluates interface
+   structures based on atomic features and energies, efficiently identifying
+   low-energy configurations.
+
  * `SchNet Pack <https://github.com/atomistic-machine-learning/schnetpack>`_:
    Deep Neural Networks for Atomistic Systems
 
@@ -121,9 +133,20 @@ Listed in alphabetical order, for want o
    Additionally, Sella can perform intrinsic reaction coordinate (IRC)
    calculations.
 
+ * `texase <https://github.com/steenlysgaard/texase>`_:
+   texase is a terminal user interface (TUI) for ASE db. It allows you
+   to navigate and manipulate ASE databases with few keystrokes in the
+   terminal.
+   
  * `TorchANI <https://github.com/aiqm/torchani>`_:
    Accurate Neural Network Potential on PyTorch
 
+ * `VRE Language <https://vre-language.readthedocs.io>`_:
+   Provides textM, a domain-specific language for atomic and molecular modeling,
+   and access to ASE Atoms, ASE constraints, ASE calculators and ASE algorithms,
+   such as molecular dynamics, energy minimum search etc., via an interpreter
+   written in Python.
+
  * `Wulffpack <https://wulffpack.materialsmodeling.org/>`_:
    Python package for making Wulff constructions, typically for finding
    equilibrium shapes of nanoparticles. WulffPack constructs both continuum
diff -pruN 3.24.0-1/doc/gallery/o2pt100.py 3.26.0-1/doc/gallery/o2pt100.py
--- 3.24.0-1/doc/gallery/o2pt100.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gallery/o2pt100.py	2025-08-12 11:26:23.000000000 +0000
@@ -14,8 +14,8 @@ middle = atoms.positions[upper_layer_idx
 
 # the dissociating oxygen... fake some dissociation curve
 gas_dist = 1.1
-max_height = 8.
-min_height = 1.
+max_height = 8.0
+min_height = 1.0
 max_dist = 6
 
 # running index for the bonds
@@ -55,16 +55,20 @@ for i in multiples:
 
 bbox = [-30, 10, 5, 25]
 
-renderer = write('o2pt100.pov', atoms,
-                 rotation='90z,-75x',
-                 bbox=bbox,
-                 show_unit_cell=0,
-                 povray_settings=dict(
-                     pause=False,
-                     canvas_width=1024,
-                     bondatoms=bonded_atoms,
-                     camera_type='perspective',
-                     transmittances=transmittances,
-                     textures=textures))
+renderer = write(
+    'o2pt100.pov',
+    atoms,
+    rotation='90z,-75x',
+    bbox=bbox,
+    show_unit_cell=0,
+    povray_settings=dict(
+        pause=False,
+        canvas_width=1024,
+        bondatoms=bonded_atoms,
+        camera_type='perspective',
+        transmittances=transmittances,
+        textures=textures,
+    ),
+)
 
 renderer.render()
diff -pruN 3.24.0-1/doc/gallery/ptable.py 3.26.0-1/doc/gallery/ptable.py
--- 3.24.0-1/doc/gallery/ptable.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gallery/ptable.py	2025-08-12 11:26:23.000000000 +0000
@@ -4,5 +4,8 @@ from ase.utils.ptable import ptable
 atoms = ptable()
 atoms.write('ptable.png')
 
-#   from ase.visualize import view
-#   view(atoms)
+# Calling "povray ptable_pov.ini" will render it with povray.
+# atoms.write('ptable_pov.pov')
+
+# from ase.visualize import view
+# view(atoms)
diff -pruN 3.24.0-1/doc/gallery/render_all_povray_styles.py 3.26.0-1/doc/gallery/render_all_povray_styles.py
--- 3.24.0-1/doc/gallery/render_all_povray_styles.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gallery/render_all_povray_styles.py	2025-08-12 11:26:23.000000000 +0000
@@ -20,14 +20,13 @@ for style in styles:
         'textures': len(atoms) * [style],
         'transparent': True,  # Transparent background
         'canvas_width': 1000,  # Width of canvas in pixels
-        'camera_type': 'orthographic angle 65',
     }
 
-    generic_projection_settings = {}
+    generic_projection_settings = {}  # keywords for io.utils.PlottingVariables
 
-    pov_object = io.write(pov_name, atoms,
-                          **generic_projection_settings,
-                          povray_settings=kwargs)
+    pov_object = io.write(
+        pov_name, atoms, **generic_projection_settings, povray_settings=kwargs
+    )
 
     if run_povray:
         pov_object.render()
diff -pruN 3.24.0-1/doc/gallery/tuning_povray_for_high_quality_images.py 3.26.0-1/doc/gallery/tuning_povray_for_high_quality_images.py
--- 3.24.0-1/doc/gallery/tuning_povray_for_high_quality_images.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gallery/tuning_povray_for_high_quality_images.py	2025-08-12 11:26:23.000000000 +0000
@@ -8,10 +8,7 @@ from ase.data.colors import jmol_colors
 from ase.io.pov import get_bondpairs, write_pov
 
 unit_cell = build.bulk('AlN', 'wurtzite', a=3.129, c=5.017)
-sqs_cell = build.cut(unit_cell,
-                     a=[1, 0, 1],
-                     b=[-2, -2, 0],
-                     c=[1, -1, -1])
+sqs_cell = build.cut(unit_cell, a=[1, 0, 1], b=[-2, -2, 0], c=[1, -1, -1])
 names = ['unit_cell', 'sqs_cell']
 list_of_atoms_obj = [unit_cell, sqs_cell]
 
@@ -19,10 +16,7 @@ list_of_atoms_obj = [unit_cell, sqs_cell
 # see the gallery to see examples of the built-in styles
 style = 'simple'
 # reverts to jmol_colors if not unspecified
-color_dict_rgb255 = {
-    'N': [23, 111, 208],
-    'Ga': [230, 83, 17]
-}
+color_dict_rgb255 = {'N': [23, 111, 208], 'Ga': [230, 83, 17]}
 
 # used to automatically guess bonds
 covalent_radius_bond_cutoff_scale = 0.9
@@ -44,7 +38,7 @@ rotation = '37x, -79y, -128z'
 kwargs = {
     'transparent': True,  # Transparent background
     'canvas_width': None,  # Width of canvas in pixels
-    'canvas_height': 720,   # Height of canvas in pixels
+    'canvas_height': 720,  # Height of canvas in pixels
     # 'image_height' : 22,
     # 'image_width'  : 102, # I think these are in atomic units
     # 'camera_dist'  : 170.0,   # Distance from camera to front atom,
@@ -96,13 +90,13 @@ def make_color_list(atoms, color_dict):
 # converting RGB255 to RGB1 for povray.
 color_dict = {}
 for symbol in color_dict_rgb255:
-    color_dict[symbol] = [val / 255. for val in color_dict_rgb255[symbol]]
+    color_dict[symbol] = [val / 255.0 for val in color_dict_rgb255[symbol]]
 
 # loop over atoms objects to render them
 for atoms, name in zip(list_of_atoms_obj, names):
-
     radius_list = make_radius_list(
-        atoms, radius_dict, radius_scale=radius_scale)
+        atoms, radius_dict, radius_scale=radius_scale
+    )
     bondpairs = get_bondpairs(atoms, radius=covalent_radius_bond_cutoff_scale)
     color_list = make_color_list(atoms, color_dict)
 
@@ -115,7 +109,7 @@ for atoms, name in zip(list_of_atoms_obj
     generic_projection_settings['radii'] = radius_list
 
     pov_name = name + '.pov'
-    povobj = write_pov(pov_name, atoms,
-                       **generic_projection_settings,
-                       povray_settings=kwargs)
+    povobj = write_pov(
+        pov_name, atoms, **generic_projection_settings, povray_settings=kwargs
+    )
     povobj.render()
diff -pruN 3.24.0-1/doc/gettingstarted/N2Cu.py 3.26.0-1/doc/gettingstarted/N2Cu.py
--- 3.24.0-1/doc/gettingstarted/N2Cu.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/N2Cu.py	2025-08-12 11:26:23.000000000 +0000
@@ -12,7 +12,7 @@ slab = fcc111('Cu', size=(4, 4, 2), vacu
 slab.calc = EMT()
 e_slab = slab.get_potential_energy()
 
-molecule = Atoms('2N', positions=[(0., 0., 0.), (0., 0., d)])
+molecule = Atoms('2N', positions=[(0.0, 0.0, 0.0), (0.0, 0.0, d)])
 molecule.calc = EMT()
 e_N2 = molecule.get_potential_energy()
 
diff -pruN 3.24.0-1/doc/gettingstarted/cluster/solution/Ag_part2_groundstate.py 3.26.0-1/doc/gettingstarted/cluster/solution/Ag_part2_groundstate.py
--- 3.24.0-1/doc/gettingstarted/cluster/solution/Ag_part2_groundstate.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/cluster/solution/Ag_part2_groundstate.py	2025-08-12 11:26:23.000000000 +0000
@@ -4,9 +4,13 @@ from ase.io import read
 
 atoms = read('opt.traj')
 
-calc = GPAW(mode='lcao', basis='sz(dzp)', txt='gpaw.txt',
-            occupations=FermiDirac(0.1),
-            setups={'Ag': '11'})
+calc = GPAW(
+    mode='lcao',
+    basis='sz(dzp)',
+    txt='gpaw.txt',
+    occupations=FermiDirac(0.1),
+    setups={'Ag': '11'},
+)
 atoms.calc = calc
 atoms.center(vacuum=4.0)
 atoms.get_potential_energy()
diff -pruN 3.24.0-1/doc/gettingstarted/manipulating_atoms/WL.py 3.26.0-1/doc/gettingstarted/manipulating_atoms/WL.py
--- 3.24.0-1/doc/gettingstarted/manipulating_atoms/WL.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/manipulating_atoms/WL.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,20 +3,21 @@ import numpy as np
 from ase import Atoms
 
 p = np.array(
-    [[0.27802511, -0.07732213, 13.46649107],
-     [0.91833251, -1.02565868, 13.41456626],
-     [0.91865997, 0.87076761, 13.41228287],
-     [1.85572027, 2.37336781, 13.56440907],
-     [3.13987926, 2.3633134, 13.4327577],
-     [1.77566079, 2.37150862, 14.66528237],
-     [4.52240322, 2.35264513, 13.37435864],
-     [5.16892729, 1.40357034, 13.42661052],
-     [5.15567324, 3.30068395, 13.4305779],
-     [6.10183518, -0.0738656, 13.27945071],
-     [7.3856151, -0.07438536, 13.40814585],
-     [6.01881192, -0.08627583, 12.1789428]])
-c = np.array([[8.490373, 0., 0.],
-              [0., 4.901919, 0.],
-              [0., 0., 26.93236]])
+    [
+        [0.27802511, -0.07732213, 13.46649107],
+        [0.91833251, -1.02565868, 13.41456626],
+        [0.91865997, 0.87076761, 13.41228287],
+        [1.85572027, 2.37336781, 13.56440907],
+        [3.13987926, 2.3633134, 13.4327577],
+        [1.77566079, 2.37150862, 14.66528237],
+        [4.52240322, 2.35264513, 13.37435864],
+        [5.16892729, 1.40357034, 13.42661052],
+        [5.15567324, 3.30068395, 13.4305779],
+        [6.10183518, -0.0738656, 13.27945071],
+        [7.3856151, -0.07438536, 13.40814585],
+        [6.01881192, -0.08627583, 12.1789428],
+    ]
+)
+c = np.array([[8.490373, 0.0, 0.0], [0.0, 4.901919, 0.0], [0.0, 0.0, 26.93236]])
 W = Atoms('4(OH2)', positions=p, cell=c, pbc=[1, 1, 0])
 W.write('WL.traj')
diff -pruN 3.24.0-1/doc/gettingstarted/manipulating_atoms/interface-h2o.py 3.26.0-1/doc/gettingstarted/manipulating_atoms/interface-h2o.py
--- 3.24.0-1/doc/gettingstarted/manipulating_atoms/interface-h2o.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/manipulating_atoms/interface-h2o.py	2025-08-12 11:26:23.000000000 +0000
@@ -25,10 +25,9 @@ write('Ni111slab2x2.png', slab)
 print(cell)
 
 # Rotate the unit cell first to get the close lattice match with the slab.
-W.set_cell([[cellW[1, 1], 0, 0],
-            [0, cellW[0, 0], 0],
-            cellW[2]],
-           scale_atoms=False)
+W.set_cell(
+    [[cellW[1, 1], 0, 0], [0, cellW[0, 0], 0], cellW[2]], scale_atoms=False
+)
 write('WL_rot_c.png', W)
 
 # Now rotate atoms just like the unit cell
diff -pruN 3.24.0-1/doc/gettingstarted/manipulating_atoms/manipulating_atoms.py 3.26.0-1/doc/gettingstarted/manipulating_atoms/manipulating_atoms.py
--- 3.24.0-1/doc/gettingstarted/manipulating_atoms/manipulating_atoms.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/manipulating_atoms/manipulating_atoms.py	2025-08-12 11:26:23.000000000 +0000
@@ -12,13 +12,12 @@ from ase.build import fcc111
 from ase.io import read, write
 
 a = 3.55
-atoms = Atoms('Ni4',
-              cell=[sqrt(2) * a, sqrt(2) * a, 1.0, 90, 90, 120],
-              pbc=(1, 1, 0),
-              scaled_positions=[(0, 0, 0),
-                                (0.5, 0, 0),
-                                (0, 0.5, 0),
-                                (0.5, 0.5, 0)])
+atoms = Atoms(
+    'Ni4',
+    cell=[sqrt(2) * a, sqrt(2) * a, 1.0, 90, 90, 120],
+    pbc=(1, 1, 0),
+    scaled_positions=[(0, 0, 0), (0.5, 0, 0), (0, 0.5, 0), (0.5, 0.5, 0)],
+)
 atoms.center(vacuum=5.0, axis=2)
 write('a1.png', atoms, rotation='-73x')
 a3 = atoms.repeat((3, 3, 2))
diff -pruN 3.24.0-1/doc/gettingstarted/surface.py 3.26.0-1/doc/gettingstarted/surface.py
--- 3.24.0-1/doc/gettingstarted/surface.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/surface.py	2025-08-12 11:26:23.000000000 +0000
@@ -5,6 +5,4 @@ from ase.io import read, write
 
 runpy.run_path('N2Cu.py')
 image = read('N2Cu.traj@-1')
-write('surface.pov',
-      image,
-      povray_settings=dict(transparent=False)).render()
+write('surface.pov', image, povray_settings=dict(transparent=False)).render()
diff -pruN 3.24.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise.py 3.26.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise.py
--- 3.24.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,8 +3,7 @@ from gpaw import GPAW
 from ase import Atoms
 from ase.optimize import BFGS
 
-atoms = Atoms('HOH',
-              positions=[[0, 0, -1], [0, 1, 0], [0, 0, 1]])
+atoms = Atoms('HOH', positions=[[0, 0, -1], [0, 1, 0], [0, 0, 1]])
 atoms.center(vacuum=3.0)
 
 calc = GPAW(mode='lcao', basis='dzp', txt='gpaw.txt')
diff -pruN 3.24.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise_aims.py 3.26.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise_aims.py
--- 3.24.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise_aims.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise_aims.py	2025-08-12 11:26:23.000000000 +0000
@@ -7,8 +7,7 @@ from ase.optimize import BFGS
 os.environ['ASE_AIMS_COMMAND'] = 'aims.x'
 os.environ['AIMS_SPECIES_DIR'] = '/home/myname/FHIaims/species_defaults/light'
 
-atoms = Atoms('HOH',
-              positions=[[0, 0, -1], [0, 1, 0], [0, 0, 1]])
+atoms = Atoms('HOH', positions=[[0, 0, -1], [0, 1, 0], [0, 0, 1]])
 
 calc = Aims(xc='LDA', compute_forces=True)
 atoms.calc = calc
diff -pruN 3.24.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise_aims_socketio.py 3.26.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise_aims_socketio.py
--- 3.24.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise_aims_socketio.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/tut02_h2o_structure/solution/optimise_aims_socketio.py	2025-08-12 11:26:23.000000000 +0000
@@ -8,13 +8,12 @@ from ase.optimize import BFGS
 os.environ['ASE_AIMS_COMMAND'] = 'aims.x'
 os.environ['AIMS_SPECIES_DIR'] = '/home/myname/FHIaims/species_defaults/light'
 
-atoms = Atoms('HOH',
-              positions=[[0, 0, -1], [0, 1, 0], [0, 0, 1]])
+atoms = Atoms('HOH', positions=[[0, 0, -1], [0, 1, 0], [0, 0, 1]])
 opt = BFGS(atoms, trajectory='opt-aims-socketio.traj')
 
-aims = Aims(xc='LDA',
-            compute_forces=True,
-            use_pimd_wrapper=('UNIX:mysocket', 31415))
+aims = Aims(
+    xc='LDA', compute_forces=True, use_pimd_wrapper=('UNIX:mysocket', 31415)
+)
 
 with SocketIOCalculator(aims, unixsocket='mysocket') as calc:
     atoms.calc = calc
diff -pruN 3.24.0-1/doc/gettingstarted/tut04_bulk/bulk.rst 3.26.0-1/doc/gettingstarted/tut04_bulk/bulk.rst
--- 3.24.0-1/doc/gettingstarted/tut04_bulk/bulk.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/tut04_bulk/bulk.rst	2025-08-12 11:26:23.000000000 +0000
@@ -234,7 +234,7 @@ as the curvature, which gives us the bul
 
 The online ASE docs already provide a tutorial on how to do this, using
 the empirical EMT potential:
-https://wiki.fysik.dtu.dk/ase/tutorials/eos/eos.html
+https://ase-lib.org/tutorials/eos/eos.html
 
 .. admonition:: Exercise
 
diff -pruN 3.24.0-1/doc/gettingstarted/tut04_bulk/solution/bulk_part1_groundstate.py 3.26.0-1/doc/gettingstarted/tut04_bulk/solution/bulk_part1_groundstate.py
--- 3.24.0-1/doc/gettingstarted/tut04_bulk/solution/bulk_part1_groundstate.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/tut04_bulk/solution/bulk_part1_groundstate.py	2025-08-12 11:26:23.000000000 +0000
@@ -3,8 +3,9 @@ from gpaw import GPAW, PW
 from ase.build import bulk
 
 atoms = bulk('Ag')
-calc = GPAW(mode=PW(350), kpts=[8, 8, 8], txt='gpaw.bulk.Ag.txt',
-            setups={'Ag': '11'})
+calc = GPAW(
+    mode=PW(350), kpts=[8, 8, 8], txt='gpaw.bulk.Ag.txt', setups={'Ag': '11'}
+)
 atoms.calc = calc
 atoms.get_potential_energy()
 calc.write('bulk.Ag.gpw')
diff -pruN 3.24.0-1/doc/gettingstarted/tut04_bulk/solution/bulk_part4_cellopt.py 3.26.0-1/doc/gettingstarted/tut04_bulk/solution/bulk_part4_cellopt.py
--- 3.24.0-1/doc/gettingstarted/tut04_bulk/solution/bulk_part4_cellopt.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/gettingstarted/tut04_bulk/solution/bulk_part4_cellopt.py	2025-08-12 11:26:23.000000000 +0000
@@ -9,12 +9,15 @@ a = 4.6
 c = 2.95
 
 # Rutile TiO2:
-atoms = crystal(['Ti', 'O'], basis=[(0, 0, 0), (0.3, 0.3, 0.0)],
-                spacegroup=136, cellpar=[a, a, c, 90, 90, 90])
+atoms = crystal(
+    ['Ti', 'O'],
+    basis=[(0, 0, 0), (0.3, 0.3, 0.0)],
+    spacegroup=136,
+    cellpar=[a, a, c, 90, 90, 90],
+)
 write('rutile.traj', atoms)
 
-calc = GPAW(mode=PW(800), kpts=[2, 2, 3],
-            txt='gpaw.rutile.txt')
+calc = GPAW(mode=PW(800), kpts=[2, 2, 3], txt='gpaw.rutile.txt')
 atoms.calc = calc
 
 opt = BFGS(ExpCellFilter(atoms), trajectory='opt.rutile.traj')
diff -pruN 3.24.0-1/doc/index.rst 3.26.0-1/doc/index.rst
--- 3.24.0-1/doc/index.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/index.rst	2025-08-12 11:26:23.000000000 +0000
@@ -63,10 +63,22 @@ Mopac_
 News
 ====
 
+* :ref:`ASE version 3.26.0 <releasenotes>` released (12 August 2025).
+
+* ASE web page moved to `ase-lib.org <https://ase-lib.org/>`_ (8 August 2025).
+
+* Workshop focusing on the `extended ASE ecosystem
+  <https://www.cecam.org/workshop-details/the-atomic-simulation-environment-ecosystem-present-and-perspectives-1373>`_
+  held at CECAM, EPFL, Switzerland June 23-27, 2025 (29 June 2025).
+
+* :ref:`ASE version 3.25.0 <releasenotes>` released (11 April 2025).
+
 * :ref:`ASE version 3.24.0 <releasenotes>` released (28 December 2024).
 
 * :ref:`ASE version 3.23.0 <releasenotes>` released (31 May 2024).
 
+* Workshop focusing on `Open Science with the Atomic Simulation Environment <https://www.cecam.org/workshop-details/open-science-with-the-atomic-simulation-environment-1245>`_ held at Daresbury Laboratory, UK April 24-28, 2023. (`Workshop site including tutorials <https://ase-workshop-2023.github.io/home/index.html>`_)
+
 * :ref:`ASE version 3.22.1 <releasenotes>` released (1 December 2021).
 
 * :ref:`ASE version 3.22.0 <releasenotes>` released (24 June 2021).
diff -pruN 3.24.0-1/doc/install.rst 3.26.0-1/doc/install.rst
--- 3.24.0-1/doc/install.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/install.rst	2025-08-12 11:26:23.000000000 +0000
@@ -38,11 +38,14 @@ Installation using system package manage
 Linux
 -----
 
-Major GNU/Linux distributions (including Debian and Ubuntu derivatives,
-Arch, Fedora, Red Hat and CentOS) have a ``python-ase`` package
+Major GNU/Linux distributions have a ``python3-ase`` (Debian and
+Ubuntu derivatives, Fedora, Red Hat) or ``python-ase`` (Arch) package
 available that you can install on your system. This will manage
 dependencies and make ASE available for all users.
 
+Red Hat requires the
+`EPEL repository <https://docs.fedoraproject.org/en-US/epel/>`_.
+
 .. note::
    Depending on the distribution, this may not be the latest
    release of ASE.
@@ -128,13 +131,12 @@ from Git.
 :Tar-file:
 
     You can get the source as a `tar-file <http://xkcd.com/1168/>`__ for the
-    latest stable release (ase-3.24.0.tar.gz_) or the latest
-    development snapshot (`<snapshot.tar.gz>`_).
+    latest stable release here: ase-3.26.0.tar.gz_
 
     Unpack and make a soft link::
 
-        $ tar -xf ase-3.24.0.tar.gz
-        $ ln -s ase-3.24.0 ase
+        $ tar -xf ase-3.26.0.tar.gz
+        $ ln -s ase-3.26.0 ase
 
     Here is a `list of tarballs <https://pypi.org/simple/ase/>`__.
 
@@ -143,7 +145,7 @@ from Git.
     Alternatively, you can get the source for the latest stable release from
     https://gitlab.com/ase/ase like this::
 
-        $ git clone -b 3.24.0 https://gitlab.com/ase/ase.git
+        $ git clone -b 3.26.0 https://gitlab.com/ase/ase.git
 
     or if you want the development version::
 
@@ -181,7 +183,7 @@ number hasn't changed.
     dates of older releases can be found there.
 
 
-.. _ase-3.24.0.tar.gz: https://pypi.org/packages/source/a/ase/ase-3.24.0.tar.gz
+.. _ase-3.26.0.tar.gz: https://pypi.org/packages/source/a/ase/ase-3.26.0.tar.gz
 
 .. index:: test
 .. _running tests:
diff -pruN 3.24.0-1/doc/releasenotes.rst 3.26.0-1/doc/releasenotes.rst
--- 3.24.0-1/doc/releasenotes.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/releasenotes.rst	2025-08-12 11:26:23.000000000 +0000
@@ -4,6 +4,8 @@
 Release notes
 =============
 
+A comprehensive list of changes can be found in the :ref:`changelog`.
+
 Git master branch
 =================
 
@@ -12,13 +14,24 @@ Git master branch
 * No changes yet
 
 
+Version 3.26.0
+==============
+
+12 August 2025: :git:`3.26.0 <../3.26.0>`
+
+* No changes yet
+
+
+Version 3.25.0
+==============
+
+11 April 2025: :git:`3.25.0 <../3.25.0>`
+
 Version 3.24.0
 ==============
 
 28 December 2024: :git:`3.24.0 <../3.24.0>`
 
-For a more comprehensive list of changes see :ref:`releasenotes_detailed`
-
 Requirements
 ------------
 
@@ -36,8 +49,8 @@ Highlights
 ----------
 
 * Major improvements to :func:`~ase.build.find_optimal_cell_shape`: improve target metric; ensure rotationally invariant results; avoid negative determinants; improved performance via vectorisation (:mr:`3404`, :mr:`3441`, :mr:`3474`). The ``norm`` argument to :func:`ase.build.supercells.get_deviation_from_optimal_cell_shape` is now deprecated.
-* Added new FiniteDifferenceCalculator which wraps other calculator for finite-difference forces and strains (:mr:`3509`)
-* Added two new MD thermostats: :class:`ase.md.bussi.Bussi` (:mr:`3350`) and :class:`ase.md.nosef_hoover_chain.NoseHooverChainNVT` (:mr:`3508`)
+* Added new :class:`~ase.calculators.fd.FiniteDifferenceCalculator`, which wraps other calculator for finite-difference forces and strains (:mr:`3509`)
+* Added two new MD thermostats: :class:`ase.md.bussi.Bussi` (:mr:`3350`) and :class:`ase.md.nose_hoover_chain.NoseHooverChainNVT` (:mr:`3508`)
 * Added atom-projected partial phonon dos to :func:`ase.phonons.Phonons.get_dos` (:mr:`3460`)
 * New module :mod:`ase.pourbaix` written to replace
   :class:`ase.phasediagram.Pourbaix` (:mr:`3280`), with improved energy definition and visualisation
@@ -289,10 +302,10 @@ Optimizers:
   (:mr:`2299`)
 
 * :func:`ase.optimize.optimize.Optimizers.irun` and
-  :func:`ase.optimize.optimize.Optimizers.run` now respect ``steps=0`` (:issue:`1183`; 
+  :func:`ase.optimize.optimize.Optimizers.run` now respect ``steps=0`` (:issue:`1183`;
   :issue:`1258`; :mr:`2922`).
 
-* Added the ``.trajectory`` attribute to :class:`ase.optimize.optimize.Dynamics` 
+* Added the ``.trajectory`` attribute to :class:`ase.optimize.optimize.Dynamics`
   (:mr:`2901`).
 
 * Fixed a bug when :class:`ase.optimize.precon.precon.PreconImages` is initialized with
@@ -2029,4 +2042,4 @@ Version 3.4.1
 
 .. toctree::
 
-  releasenotes_detailed.rst
+  changelog.rst
diff -pruN 3.24.0-1/doc/releasenotes_detailed.rst 3.26.0-1/doc/releasenotes_detailed.rst
--- 3.24.0-1/doc/releasenotes_detailed.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/releasenotes_detailed.rst	1970-01-01 00:00:00.000000000 +0000
@@ -1,152 +0,0 @@
-.. _releasenotes_detailed:
-
-======================
-Detailed release notes
-======================
-
-Git master branch
-=================
-
-Requirements
-------------
-
-* The minimum supported Python version has increased to 3.9 (:mr:`3473`)
-* Support numpy 2 (:mr:`3398`, :mr:`3400`, :mr:`3402`)
-* Support spglib 2.5.0 (:mr:`3452`)
-
-Atoms
------
-* New method :func:`~ase.Atoms.get_number_of_degrees_of_freedom()` (:mr:`3380`)
-* New methods :func:`~ase.Atoms.get_kinetic_stress()`, :func:`~ase.Atoms.get_kinetic_stresses()` (:mr:`3362`)
-* Prevent truncation when printing Atoms objects with 1000 or more atoms (:mr:`2518`)
-
-DB
---
-* Ensure correct float format when writing to Postgres database (:mr:`3475`)
-
-Structure tools
----------------
-
-* Add atom tagging to ``ase.build.general_surface`` (:mr:`2773`)
-* Fix bug where code could return the wrong lattice when trying to fix the handedness of a 2D lattice  (:mr:`3387`)
-* Major improvements to :func:`~ase.build.find_optimal_cell_shape`: improve target metric; ensure rotationally invariant results; avoid negative determinants; improved performance via vectorisation (:mr:`3404`, :mr:`3441`, :mr:`3474`). The ``norm`` argument to :func:`~ase.build.supercells.get_deviation_from_optimal_cell_shape` is now deprecated.
-* Performance improvements to :class:`ase.spacegroup.spacegroup.Spacegroup` (:mr:`3434`, :mr:`3439`, :mr:`3448`)
-* Deprecated :func:`ase.spacegroup.spacegroup.get_spacegroup` as results can be misleading (:mr:`3455`).
-  
-
-Calculators / IO
-----------------
-
-* Amber: Fix scaling of velocities in restart files (:mr:`3427`)
-* Amber: Raise an error if cell is orthorhombic (:mr:`3443`)
-* CASTEP
-
-  - **BREAKING** Removed legacy ``read_cell`` and ``write_cell`` functions from ase.io.castep. (:mr:`3435`)
-  - .castep file reader bugfix for Windows (:mr:`3379`), testing improved (:mr:`3375`)
-  - fix read from Castep geometry optimisation with stress only (:mr:`3445`)
-
-* EAM: Fix calculations with self.form = "eam" (:mr:`3399`)
-* FHI-aims
-  
-  - make free_energy the default energy (:mr:`3406`)
-  - add legacy DFPT parser hook (:mr:`3495`)
-
-* FileIOSocketClientLauncher: Fix an unintended API change (:mr:`3453`)
-* FiniteDifferenceCalculator: added new calculator which wraps other calculator for finite-difference forces and strains (:mr:`3509`)
-* GenericFileIOCalculator fix interaction with SocketIO (:mr:`3381`)
-* LAMMPS
-
-  - fixed a bug reading dump file with only one atom (:mr:`3423`)
-  - support initial charges (:mr:`2846`, :mr:`3431`)
-
-* MixingCalculator: remove requirement that mixed calculators have common ``implemented_properties`` (:mr:`3480`)
-* MOPAC: Improve version-number parsing (:mr:`3483`)
-* MorsePotential: Add stress (by finite differences) (:mr:`3485`)
-* NWChem: fixed reading files from other directories (:mr:`3418`)
-* Octopus: Improved IO testing (:mr:`3465`)
-* ONETEP calculator: allow ``pseudo_path`` to be set in config (:mr:`3385`)
-* Orca: Only parse dipoles if COM is found. (:mr:`3426`)
-* Quantum Espresso
-
-  - allow arbitrary k-point lists (:mr:`3339`)
-  - support keys from EPW (:mr:`3421`)
-  - Fix path handling when running remote calculations from Windows (:mr:`3464`)
-
-* Siesta: support version 5.0 (:mr:`3464`)
-* Turbomole: fixed formatting of "density convergence" parameter (:mr:`3412`)
-* VASP
-
-  - Fixed a bug handling the ICHAIN tag from VTST (:mr:`3415`)
-  - Fixed bugs in CHG file writing (:mr:`3428`) and CHGCAR reading (:mr:`3447`)
-  - Fix parsing POSCAR scale-factor line that includes a comment (:mr:`3487`)
-  - Support use of unknown INCAR keys (:mr:`3488`)
-  - Drop "INCAR created by Atomic Simulation Environment" header (:mr:`3488`)
-  - Drop 1-space indentation of INCAR file (:mr:`3488`)
-  - Use attached atoms if no atom argument provided to :func:`ase.calculators.vasp.Vasp.calculate` (:mr:`3491`)
-
-GUI
----
-* Refactoring of :class:`ase.gui.view.View` to improve API for external projects (:mr:`3419`)
-* Force lines to appear black (:mr:`3459`)
-* Fix missing Alt+X/Y/Z/1/2/3 shortcuts to set view direction (:mr:`3482`)
-* Fix incorrect frame number after using Page-Up/Page-Down controls (:mr:`3481`)
-* Fix incorrect double application of `repeat` to `energy` in GUI (:mr:`3492`)
-
-Molecular Dynamics
-------------------
-
-* Added Bussi thermostat :class:`ase.md.bussi.Bussi` (:mr:`3350`)
-* Added Nose-Hoover chain NVT thermostat :class:`ase.md.nose_hoover_chain.NoseHooverChainNVT` (:mr:`3508`)
-* Improve ``force_temperature`` to work with constraints (:mr:`3393`)
-* Add ``**kwargs`` to MolecularDynamics, passed to parent Dynamics (:mr:`3403`)
-* Support modern Numpy PRNGs in Andersen thermostat (:mr:`3454`)
-
-Optimizers
-----------
-* **BREAKING** The ``master`` parameter to each Optimizer is now passed via ``**kwargs`` and so becomes keyword-only. (:mr:`3424`)
-* Pass ``comm`` to BFGS and CellAwareBFGS as a step towards cleaner parallelism (:mr:`3397`)
-* **BREAKING** Removed deprecated ``force_consistent`` option from Optimizer (:mr:`3424`)
-
-Phonons
--------
-
-* Fix scaling of phonon amplitudes (:mr:`3438`)
-* Implement atom-projected PDOS, deprecate :func:`ase.phonons.Phonons.dos` in favour of :func:`ase.phonons.Phonons.get_dos` (:mr:`3460`)
-* Suppress warnings about imaginary frequencies unless :func:`ase.phonons.Phonons.get_dos` is called with new parameter ``verbose=True`` (:mr:`3461`)
-
-Pourbaix (:mr:`3280`)
----------------------
-
-* New module :mod:`ase.pourbaix` written to replace :class:`ase.phasediagram.Pourbaix`
-* Improved energy definition and diagram generation method
-* Improved visualisation
-
-Spectrum
---------
-* **BREAKING** :class:`ase.spectrum.band_structure.BandStructurePlot`: the ``plot_with_colors()`` has been removed and its features merged into the ``plot()`` method.
-
-Misc
-----
-* Cleaner bandgap description from :class:`ase.dft.bandgap.GapInfo` (:mr:`3451`)
-
-Documentation
--------------
-* The "legacy functionality" section has been removed (:mr:`3386`)
-* Other minor improvements and additions (:mr:`2520`, :mr:`3377`, :mr:`3388`, :mr:`3389`, :mr:`3394`, :mr:`3395`, :mr:`3407`, :mr:`3413`, :mr:`3416`, :mr:`3446`, :mr:`3458`, :mr:`3468`)
-
-Testing
--------
-* Remove some dangling open files (:mr:`3384`)
-* Ensure all test modules are properly packaged (:mr:`3489`)
-
-Units
------
-* Added 2022 CODATA values (:mr:`3450`)
-* Fixed value of vacuum magnetic permeability ``_mu0`` in (non-default) CODATA 2018 (:mr:`3486`)
-
-Maintenance and dev-ops
------------------------
-* Set up ruff linter (:mr:`3392`, :mr:`3420`)
-* Further linting (:mr:`3396`, :mr:`3425`, :mr:`3430`, :mr:`3433`, :mr:`3469`, :mr:`3520`)
-* Refactoring of ``ase.build.bulk`` (:mr:`3390`), ``ase.spacegroup.spacegroup`` (:mr:`3429`)
-
diff -pruN 3.24.0-1/doc/static/ase.css 3.26.0-1/doc/static/ase.css
--- 3.24.0-1/doc/static/ase.css	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/static/ase.css	1970-01-01 00:00:00.000000000 +0000
@@ -1,13 +0,0 @@
-@import url("css/theme.css");
-
-div.asetop {text-align: right}
-
-dl.function {background-color: #E0E0FF}
-dl.method {background-color: #E0E0FF}
-dl.class {background-color: #E0E0FF}
-
-img {border: none;}
-img.center{display:block; margin-left:auto; margin-right:auto; text-align: center;}
-.wy-nav-content {
-    max-width: none;
-}
diff -pruN 3.24.0-1/doc/templates/breadcrumbs.html 3.26.0-1/doc/templates/breadcrumbs.html
--- 3.24.0-1/doc/templates/breadcrumbs.html	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/templates/breadcrumbs.html	1970-01-01 00:00:00.000000000 +0000
@@ -1,8 +0,0 @@
-<div class=asetop>
-<a href={{ pathto('genindex') }}>index</a>
-| <a href={{ pathto('py-modindex') }}>modules</a>
-| <a href=https://gitlab.com/ase/ase>gitlab</a>
-{% if sourcename %}
-| <a href="https://gitlab.com/ase/ase/blob/master/doc/{{ sourcename[:-4] }}">page source</a>
-{% endif %}
-</div>
diff -pruN 3.24.0-1/doc/tutorials/N2.py 3.26.0-1/doc/tutorials/N2.py
--- 3.24.0-1/doc/tutorials/N2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/N2.py	2025-08-12 11:26:23.000000000 +0000
@@ -6,7 +6,7 @@ atom.calc = EMT()
 e_atom = atom.get_potential_energy()
 
 d = 1.1
-molecule = Atoms('2N', [(0., 0., 0.), (0., 0., d)])
+molecule = Atoms('2N', [(0.0, 0.0, 0.0), (0.0, 0.0, d)])
 molecule.calc = EMT()
 e_molecule = molecule.get_potential_energy()
 
diff -pruN 3.24.0-1/doc/tutorials/N2Cu-Dissociation1.py 3.26.0-1/doc/tutorials/N2Cu-Dissociation1.py
--- 3.24.0-1/doc/tutorials/N2Cu-Dissociation1.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/N2Cu-Dissociation1.py	2025-08-12 11:26:23.000000000 +0000
@@ -14,8 +14,9 @@ slab.set_pbc((1, 1, 0))
 # Initial state.
 # Add the N2 molecule oriented at 60 degrees:
 d = 1.10  # N2 bond length
-N2mol = Atoms('N2', positions=[[0.0, 0.0, 0.0],
-                               [0.5 * 3**0.5 * d, 0.5 * d, 0.0]])
+N2mol = Atoms(
+    'N2', positions=[[0.0, 0.0, 0.0], [0.5 * 3**0.5 * d, 0.5 * d, 0.0]]
+)
 add_adsorbate(slab, N2mol, height=1.0, position='fcc')
 
 # Use the EMT calculator for the forces and energies:
diff -pruN 3.24.0-1/doc/tutorials/N2Ru-Dissociation1.py 3.26.0-1/doc/tutorials/N2Ru-Dissociation1.py
--- 3.24.0-1/doc/tutorials/N2Ru-Dissociation1.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/N2Ru-Dissociation1.py	2025-08-12 11:26:23.000000000 +0000
@@ -9,19 +9,20 @@ from ase.optimize import QuasiNewton
 # Set up a (3 x 3) two layer slab of Ru:
 a = 2.70
 c = 1.59 * a
-sqrt3 = 3. ** .5
-bulk = Atoms('2Cu', [(0., 0., 0.), (1. / 3, 1. / 3, -0.5 * c)],
-             tags=(1, 1),
-             pbc=(1, 1, 0))
-bulk.set_cell([(a, 0, 0),
-               (a / 2, sqrt3 * a / 2, 0),
-               (0, 0, 1)])
+sqrt3 = 3.0**0.5
+bulk = Atoms(
+    '2Cu',
+    [(0.0, 0.0, 0.0), (1.0 / 3, 1.0 / 3, -0.5 * c)],
+    tags=(1, 1),
+    pbc=(1, 1, 0),
+)
+bulk.set_cell([(a, 0, 0), (a / 2, sqrt3 * a / 2, 0), (0, 0, 1)])
 slab = bulk.repeat((4, 4, 1))
 
 # Initial state.
 # Add the molecule:
-x = a / 2.
-y = a * 3. ** .5 / 6.
+x = a / 2.0
+y = a * 3.0**0.5 / 6.0
 z = 1.8
 d = 1.10  # N2 bond length
 
diff -pruN 3.24.0-1/doc/tutorials/acn_equil/acn_equil.py 3.26.0-1/doc/tutorials/acn_equil/acn_equil.py
--- 3.24.0-1/doc/tutorials/acn_equil/acn_equil.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/acn_equil/acn_equil.py	2025-08-12 11:26:23.000000000 +0000
@@ -7,9 +7,7 @@ from ase.constraints import FixLinearTri
 from ase.io import Trajectory
 from ase.md import Langevin
 
-pos = [[0, 0, -r_mec],
-       [0, 0, 0],
-       [0, 0, r_cn]]
+pos = [[0, 0, -r_mec], [0, 0, 0], [0, 0, r_cn]]
 atoms = Atoms('CCN', positions=pos)
 atoms.rotate(30, 'x')
 
@@ -21,7 +19,7 @@ atoms.set_masses(masses)
 # Determine side length of a box with the density of acetonitrile at 298 K
 # Density in g/Ang3 (https://pubs.acs.org/doi/10.1021/je00001a006)
 d = 0.776 / 1e24
-L = ((masses.sum() / units.mol) / d)**(1 / 3.)
+L = ((masses.sum() / units.mol) / d) ** (1 / 3.0)
 # Set up box of 27 acetonitrile molecules
 atoms.set_cell((L, L, L))
 atoms.center()
@@ -31,17 +29,20 @@ atoms.set_pbc(True)
 # Set constraints for rigid triatomic molecules
 nm = 27
 atoms.constraints = FixLinearTriatomic(
-    triples=[(3 * i, 3 * i + 1, 3 * i + 2)
-             for i in range(nm)])
+    triples=[(3 * i, 3 * i + 1, 3 * i + 2) for i in range(nm)]
+)
 
 tag = 'acn_27mol_300K'
 atoms.calc = ACN(rc=np.min(np.diag(atoms.cell)) / 2)
 
 # Create Langevin object
-md = Langevin(atoms, 1 * units.fs,
-              temperature=300 * units.kB,
-              friction=0.01,
-              logfile=tag + '.log')
+md = Langevin(
+    atoms,
+    1 * units.fs,
+    temperature=300 * units.kB,
+    friction=0.01,
+    logfile=tag + '.log',
+)
 
 traj = Trajectory(tag + '.traj', 'w', atoms)
 md.attach(traj.write, interval=1)
@@ -52,17 +53,20 @@ atoms.set_constraint()
 atoms = atoms.repeat((2, 2, 2))
 nm = 216
 atoms.constraints = FixLinearTriatomic(
-    triples=[(3 * i, 3 * i + 1, 3 * i + 2)
-             for i in range(nm)])
+    triples=[(3 * i, 3 * i + 1, 3 * i + 2) for i in range(nm)]
+)
 
 tag = 'acn_216mol_300K'
 atoms.calc = ACN(rc=np.min(np.diag(atoms.cell)) / 2)
 
 # Create Langevin object
-md = Langevin(atoms, 2 * units.fs,
-              temperature=300 * units.kB,
-              friction=0.01,
-              logfile=tag + '.log')
+md = Langevin(
+    atoms,
+    2 * units.fs,
+    temperature=300 * units.kB,
+    friction=0.01,
+    logfile=tag + '.log',
+)
 
 traj = Trajectory(tag + '.traj', 'w', atoms)
 md.attach(traj.write, interval=1)
diff -pruN 3.24.0-1/doc/tutorials/constraints/diffusion.py 3.26.0-1/doc/tutorials/constraints/diffusion.py
--- 3.24.0-1/doc/tutorials/constraints/diffusion.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/constraints/diffusion.py	2025-08-12 11:26:23.000000000 +0000
@@ -9,7 +9,10 @@ a = images[0] + images[1] + images[2] +
 del a.constraints
 a *= (2, 1, 1)
 a.set_cell(images[0].get_cell())
-renderer = write('diffusion-path.pov', a,
-                 rotation='-90x',
-                 povray_settings=dict(transparent=False))
+renderer = write(
+    'diffusion-path.pov',
+    a,
+    rotation='-90x',
+    povray_settings=dict(transparent=False),
+)
 renderer.render()
diff -pruN 3.24.0-1/doc/tutorials/db/bulk.py 3.26.0-1/doc/tutorials/db/bulk.py
--- 3.24.0-1/doc/tutorials/db/bulk.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/db/bulk.py	2025-08-12 11:26:23.000000000 +0000
@@ -10,6 +10,6 @@ for symb in ['Al', 'Ni', 'Cu', 'Pd', 'Ag
     eos = calculate_eos(atoms)
     v, e, B = eos.fit()  # find minimum
     # Do one more calculation at the minimu and write to database:
-    atoms.cell *= (v / atoms.get_volume())**(1 / 3)
+    atoms.cell *= (v / atoms.get_volume()) ** (1 / 3)
     atoms.get_potential_energy()
     db.write(atoms, bm=B)
diff -pruN 3.24.0-1/doc/tutorials/db/ea.py 3.26.0-1/doc/tutorials/db/ea.py
--- 3.24.0-1/doc/tutorials/db/ea.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/db/ea.py	2025-08-12 11:26:23.000000000 +0000
@@ -4,8 +4,10 @@ refs = connect('refs.db')
 db = connect('ads.db')
 
 for row in db.select():
-    ea = (row.energy -
-          refs.get(formula=row.ads).energy -
-          refs.get(layers=row.layers, surf=row.surf).energy)
+    ea = (
+        row.energy
+        - refs.get(formula=row.ads).energy
+        - refs.get(layers=row.layers, surf=row.surf).energy
+    )
     h = row.positions[-1, 2] - row.positions[-2, 2]
     db.update(row.id, height=h, ea=ea)
diff -pruN 3.24.0-1/doc/tutorials/db/run.py 3.26.0-1/doc/tutorials/db/run.py
--- 3.24.0-1/doc/tutorials/db/run.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/db/run.py	2025-08-12 11:26:23.000000000 +0000
@@ -14,10 +14,12 @@ for name in ['bulk.db', 'ads.db', 'refs.
 for filename in ['bulk.py', 'ads.py', 'refs.py']:
     runpy.run_path(filename)
 
-for cmd in ['ase db ads.db ads=clean --insert-into refs.db',
-            'ase db ads.db ads=clean --delete --yes',
-            'ase db ads.db pbc=FFF --insert-into refs.db',
-            'ase db ads.db pbc=FFF --delete --yes']:
+for cmd in [
+    'ase db ads.db ads=clean --insert-into refs.db',
+    'ase db ads.db ads=clean --delete --yes',
+    'ase db ads.db pbc=FFF --insert-into refs.db',
+    'ase db ads.db pbc=FFF --delete --yes',
+]:
     main(args=cmd.split()[1:])
 
 runpy.run_path('ea.py')
@@ -26,8 +28,7 @@ runpy.run_path('ea.py')
 for n in [1, 2, 3]:
     a = read(f'ads.db@Cu{n}O')[0]
     a *= (2, 2, 1)
-    renderer = write(f'cu{n}o.pov', a,
-                     rotation='-80x')
+    renderer = write(f'cu{n}o.pov', a, rotation='-80x')
     renderer.render()
 
 # A bit of testing:
diff -pruN 3.24.0-1/doc/tutorials/defects/periodic-images.py 3.26.0-1/doc/tutorials/defects/periodic-images.py
--- 3.24.0-1/doc/tutorials/defects/periodic-images.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/defects/periodic-images.py	2025-08-12 11:26:23.000000000 +0000
@@ -6,8 +6,7 @@ import matplotlib.pyplot as plt
 import numpy as np
 
 
-class CellFigure():
-
+class CellFigure:
     def __init__(self, dim):
         """
         Set up a figure for visualizing a cell metric.
@@ -20,8 +19,15 @@ class CellFigure():
         self.ax.set_ylim(-dim, 1.5 * dim)
         self.ax.set_aspect('equal')
 
-    def add_cell(self, cell, offset=[0, 0], fill_color=None,
-                 atom=None, radius=0.1, atom_color='orange'):
+    def add_cell(
+        self,
+        cell,
+        offset=[0, 0],
+        fill_color=None,
+        atom=None,
+        radius=0.1,
+        atom_color='orange',
+    ):
         """
         Draw a cell, optionally filled and including an atom.
         """
@@ -31,11 +37,11 @@ class CellFigure():
             vectors.append(np.dot(cell, xv + offset))
         vectors = np.array(vectors)
         from matplotlib.patches import Circle, Polygon
+
         if fill_color is not None:
-            self.ax.add_patch(Polygon(vectors,
-                                      closed=True,
-                                      color=fill_color,
-                                      alpha=0.4))
+            self.ax.add_patch(
+                Polygon(vectors, closed=True, color=fill_color, alpha=0.4)
+            )
         for points in vectors:
             self.ax.plot(vectors.T[0], vectors.T[1], c='k', ls='-')
         if atom:
@@ -48,6 +54,7 @@ class CellFigure():
         Draw an arrow typically symbolizing a neighbor connection.
         """
         from matplotlib.patches import Arrow
+
         pos = np.dot(cell, xvec)
         dir = np.dot(cell, xdir)
         self.ax.add_patch(Arrow(pos[0], pos[1], dir[0], dir[1], width=0.2))
@@ -85,7 +92,8 @@ for i, j in product(range(-dim, dim + 1)
     fill_color = 'blue' if i == 0 and j == 0 else None
     myfig.add_cell(prim, [i, j], atom=atompos, fill_color=fill_color)
 myfig.annotate_figure(
-    'rectangular lattice with a 2:1 aspect ratio\n$r_1 = a/2$, $Z_1=2$')
+    'rectangular lattice with a 2:1 aspect ratio\n$r_1 = a/2$, $Z_1=2$'
+)
 plt.savefig('periodic-images-2.svg', bbox_inches='tight')
 
 # Figure 3
diff -pruN 3.24.0-1/doc/tutorials/defects/score-size.py 3.26.0-1/doc/tutorials/defects/score-size.py
--- 3.24.0-1/doc/tutorials/defects/score-size.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/defects/score-size.py	2025-08-12 11:26:23.000000000 +0000
@@ -18,8 +18,12 @@ for fname in glob.glob('Popt*2*.json'):
         y.append(rec['dev'])
 
     plt.figure(figsize=(4, 3))
-    plt.text(1950, 0.35,
-             tag.replace('2', r' $\rightarrow$ '), horizontalalignment='right')
+    plt.text(
+        1950,
+        0.35,
+        tag.replace('2', r' $\rightarrow$ '),
+        horizontalalignment='right',
+    )
     plt.xlabel(r'Number of primitive unit cells $N_{uc}$')
     plt.ylabel(r'Optimality measure $\bar \Delta$')
     plt.axis([0, 2000, -0.025, 0.4])
diff -pruN 3.24.0-1/doc/tutorials/defects/supercells.py 3.26.0-1/doc/tutorials/defects/supercells.py
--- 3.24.0-1/doc/tutorials/defects/supercells.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/defects/supercells.py	2025-08-12 11:26:23.000000000 +0000
@@ -12,6 +12,7 @@ def vertices(cell):
     Set up vertices for a cell metric.
     """
     from scipy.spatial import Voronoi
+
     I = np.indices((3, 3, 3)).reshape((3, 27)) - 1
     G = np.dot(cell, I).T
     vor = Voronoi(G)
@@ -19,13 +20,12 @@ def vertices(cell):
     for vertices, points in zip(vor.ridge_vertices, vor.ridge_points):
         if -1 not in vertices and 13 in points:
             normal = G[points].sum(0)
-            normal /= (normal ** 2).sum() ** 0.5
+            normal /= (normal**2).sum() ** 0.5
             vert1.append((vor.vertices[vertices], normal))
     return vert1
 
 
-class CellFigure():
-
+class CellFigure:
     def __init__(self, dim, azim, elev):
         """
         Set up a figure for visualizing a cell metric.
@@ -68,16 +68,19 @@ class CellFigure():
         # plot side faces of unit cell
         X, Y = np.meshgrid([0, uc], [0, uc])
         Z = np.zeros((2, 2)) + uc
-        self.ax.plot_surface(X, Y, Z,
-                             color='blue', alpha=.5, linewidth=0, zorder=1)
+        self.ax.plot_surface(
+            X, Y, Z, color='blue', alpha=0.5, linewidth=0, zorder=1
+        )
         X, Z = np.meshgrid([0, uc], [0, uc])
         Y = np.zeros((2, 2)) + uc
-        self.ax.plot_surface(X, Y, Z,
-                             color='blue', alpha=.5, linewidth=0, zorder=1)
+        self.ax.plot_surface(
+            X, Y, Z, color='blue', alpha=0.5, linewidth=0, zorder=1
+        )
         Y, Z = np.meshgrid([0, uc], [0, uc])
         X = np.zeros((2, 2)) + uc
-        self.ax.plot_surface(X, Y, Z,
-                             color='blue', alpha=.5, linewidth=0, zorder=1)
+        self.ax.plot_surface(
+            X, Y, Z, color='blue', alpha=0.5, linewidth=0, zorder=1
+        )
 
     def add_atom(self, x0, y0, z0, radius=0.06):
         """
@@ -88,9 +91,16 @@ class CellFigure():
         x = x0 + radius * np.outer(np.cos(u), np.sin(v))
         y = y0 + radius * np.outer(np.sin(u), np.sin(v))
         z = z0 + radius * np.outer(np.ones(np.size(u)), np.cos(v))
-        self.ax.plot_surface(x, y, z,
-                             rstride=4, cstride=4,
-                             color='orange', linewidth=0.1, alpha=0.5)
+        self.ax.plot_surface(
+            x,
+            y,
+            z,
+            rstride=4,
+            cstride=4,
+            color='orange',
+            linewidth=0.1,
+            alpha=0.5,
+        )
 
     def annotate_figure(self, text):
         """
diff -pruN 3.24.0-1/doc/tutorials/deltacodesdft/calculate.py 3.26.0-1/doc/tutorials/deltacodesdft/calculate.py
--- 3.24.0-1/doc/tutorials/deltacodesdft/calculate.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/deltacodesdft/calculate.py	2025-08-12 11:26:23.000000000 +0000
@@ -6,7 +6,7 @@ for symbol in ['Al', 'Ni', 'Cu', 'Pd', '
     traj = Trajectory(f'{symbol}.traj', 'w')
     for s in range(94, 108, 2):
         atoms = dcdft[symbol]
-        atoms.set_cell(atoms.cell * (s / 100)**(1 / 3), scale_atoms=True)
+        atoms.set_cell(atoms.cell * (s / 100) ** (1 / 3), scale_atoms=True)
         atoms.calc = EMT()
         atoms.get_potential_energy()
         traj.write(atoms)
diff -pruN 3.24.0-1/doc/tutorials/deltacodesdft/fit.py 3.26.0-1/doc/tutorials/deltacodesdft/fit.py
--- 3.24.0-1/doc/tutorials/deltacodesdft/fit.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/deltacodesdft/fit.py	2025-08-12 11:26:23.000000000 +0000
@@ -22,9 +22,11 @@ data = {}  # Dict[str, Dict[str, float]]
 for path in Path().glob('*.traj'):
     symbol = path.stem
     e0, v0, B, Bp = fit(symbol)
-    data[symbol] = {'emt_energy': e0,
-                    'emt_volume': v0,
-                    'emt_B': B,
-                    'emt_Bp': Bp}
+    data[symbol] = {
+        'emt_energy': e0,
+        'emt_volume': v0,
+        'emt_B': B,
+        'emt_Bp': Bp,
+    }
 
 Path('fit.json').write_text(json.dumps(data))
diff -pruN 3.24.0-1/doc/tutorials/deltacodesdft/tables.py 3.26.0-1/doc/tutorials/deltacodesdft/tables.py
--- 3.24.0-1/doc/tutorials/deltacodesdft/tables.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/deltacodesdft/tables.py	2025-08-12 11:26:23.000000000 +0000
@@ -23,35 +23,44 @@ for name in ['volume', 'B', 'Bp']:
     with open(name + '.csv', 'w') as f:
         print('# symbol, emt, exp, wien2k', file=f)
         for symbol, dct in data.items():
-            values = [dct[code + '_' + name]
-                      for code in ['emt', 'exp', 'wien2k']]
+            values = [
+                dct[code + '_' + name] for code in ['emt', 'exp', 'wien2k']
+            ]
             if name == 'B':
                 values = [val * 1e24 / kJ for val in values]
-            print(f'{symbol},',
-                  ', '.join(f'{value:.2f}' for value in values),
-                  file=f)
+            print(
+                f'{symbol},',
+                ', '.join(f'{value:.2f}' for value in values),
+                file=f,
+            )
 
 with open('delta.csv', 'w') as f:
     print('# symbol, emt-exp, emt-wien2k, exp-wien2k', file=f)
     for symbol, dct in data.items():
         # Get v0, B, Bp:
-        emt, exp, wien2k = ((dct[code + '_volume'],
-                             dct[code + '_B'],
-                             dct[code + '_Bp'])
-                            for code in ['emt', 'exp', 'wien2k'])
-        print(f'{symbol},',
-              '{:.1f}, {:.1f}, {:.1f}'.format(delta(*emt, *exp) * 1000,
-                                              delta(*emt, *wien2k) * 1000,
-                                              delta(*exp, *wien2k) * 1000),
-              file=f)
+        emt, exp, wien2k = (
+            (dct[code + '_volume'], dct[code + '_B'], dct[code + '_Bp'])
+            for code in ['emt', 'exp', 'wien2k']
+        )
+        print(
+            f'{symbol},',
+            '{:.1f}, {:.1f}, {:.1f}'.format(
+                delta(*emt, *exp) * 1000,
+                delta(*emt, *wien2k) * 1000,
+                delta(*exp, *wien2k) * 1000,
+            ),
+            file=f,
+        )
 
         if symbol == 'Pt':
             va = min(emt[0], exp[0], wien2k[0])
             vb = max(emt[0], exp[0], wien2k[0])
             v = np.linspace(0.94 * va, 1.06 * vb)
-            for (v0, B, Bp), code in [(emt, 'EMT'),
-                                      (exp, 'experiment'),
-                                      (wien2k, 'WIEN2k')]:
+            for (v0, B, Bp), code in [
+                (emt, 'EMT'),
+                (exp, 'experiment'),
+                (wien2k, 'WIEN2k'),
+            ]:
                 plt.plot(v, birchmurnaghan(v, 0.0, B, Bp, v0), label=code)
             e0 = dct['emt_energy']
             V = []
diff -pruN 3.24.0-1/doc/tutorials/dimensionality/isolation_example.py 3.26.0-1/doc/tutorials/dimensionality/isolation_example.py
--- 3.24.0-1/doc/tutorials/dimensionality/isolation_example.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/dimensionality/isolation_example.py	2025-08-12 11:26:23.000000000 +0000
@@ -18,7 +18,7 @@ atoms.cell[2, 2] = 14.0
 
 # isolate each component in the whole material
 result = isolate_components(atoms)
-print("counts:", [(k, len(v)) for k, v in sorted(result.items())])
+print('counts:', [(k, len(v)) for k, v in sorted(result.items())])
 
 for dim, components in result.items():
     for atoms in components:
diff -pruN 3.24.0-1/doc/tutorials/eos/eos1.py 3.26.0-1/doc/tutorials/eos/eos1.py
--- 3.24.0-1/doc/tutorials/eos/eos1.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/eos/eos1.py	2025-08-12 11:26:23.000000000 +0000
@@ -6,10 +6,9 @@ from ase.io.trajectory import Trajectory
 
 a = 4.0  # approximate lattice constant
 b = a / 2
-ag = Atoms('Ag',
-           cell=[(0, b, b), (b, 0, b), (b, b, 0)],
-           pbc=1,
-           calculator=EMT())  # use EMT potential
+ag = Atoms(
+    'Ag', cell=[(0, b, b), (b, 0, b), (b, b, 0)], pbc=1, calculator=EMT()
+)  # use EMT potential
 cell = ag.get_cell()
 traj = Trajectory('Ag.traj', 'w')
 for x in np.linspace(0.95, 1.05, 5):
diff -pruN 3.24.0-1/doc/tutorials/ga/basic_example_create_database.py 3.26.0-1/doc/tutorials/ga/basic_example_create_database.py
--- 3.24.0-1/doc/tutorials/ga/basic_example_create_database.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/basic_example_create_database.py	2025-08-12 11:26:23.000000000 +0000
@@ -17,23 +17,25 @@ slab.set_constraint(FixAtoms(mask=len(sl
 # and three spanning vectors (v1, v2, v3)
 pos = slab.get_positions()
 cell = slab.get_cell()
-p0 = np.array([0., 0., max(pos[:, 2]) + 2.])
+p0 = np.array([0.0, 0.0, max(pos[:, 2]) + 2.0])
 v1 = cell[0, :] * 0.8
 v2 = cell[1, :] * 0.8
 v3 = cell[2, :]
-v3[2] = 3.
+v3[2] = 3.0
 
 # Define the composition of the atoms to optimize
 atom_numbers = 2 * [47] + 2 * [79]
 
 # define the closest distance two atoms of a given species can be to each other
 unique_atom_types = get_all_atom_types(slab, atom_numbers)
-blmin = closest_distances_generator(atom_numbers=unique_atom_types,
-                                    ratio_of_covalent_radii=0.7)
+blmin = closest_distances_generator(
+    atom_numbers=unique_atom_types, ratio_of_covalent_radii=0.7
+)
 
 # create the starting population
-sg = StartGenerator(slab, atom_numbers, blmin,
-                    box_to_place_in=[p0, [v1, v2, v3]])
+sg = StartGenerator(
+    slab, atom_numbers, blmin, box_to_place_in=[p0, [v1, v2, v3]]
+)
 
 # generate the starting population
 population_size = 20
@@ -43,9 +45,9 @@ starting_population = [sg.get_new_candid
 # view(starting_population)        # to see the starting population
 
 # create the database to store information in
-d = PrepareDB(db_file_name=db_file,
-              simulation_cell=slab,
-              stoichiometry=atom_numbers)
+d = PrepareDB(
+    db_file_name=db_file, simulation_cell=slab, stoichiometry=atom_numbers
+)
 
 for a in starting_population:
     d.add_unrelaxed_candidate(a)
diff -pruN 3.24.0-1/doc/tutorials/ga/basic_example_main_run.py 3.26.0-1/doc/tutorials/ga/basic_example_main_run.py
--- 3.24.0-1/doc/tutorials/ga/basic_example_main_run.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/basic_example_main_run.py	2025-08-12 11:26:23.000000000 +0000
@@ -26,20 +26,25 @@ atom_numbers_to_optimize = da.get_atom_n
 n_to_optimize = len(atom_numbers_to_optimize)
 slab = da.get_slab()
 all_atom_types = get_all_atom_types(slab, atom_numbers_to_optimize)
-blmin = closest_distances_generator(all_atom_types,
-                                    ratio_of_covalent_radii=0.7)
+blmin = closest_distances_generator(all_atom_types, ratio_of_covalent_radii=0.7)
 
-comp = InteratomicDistanceComparator(n_top=n_to_optimize,
-                                     pair_cor_cum_diff=0.015,
-                                     pair_cor_max=0.7,
-                                     dE=0.02,
-                                     mic=False)
+comp = InteratomicDistanceComparator(
+    n_top=n_to_optimize,
+    pair_cor_cum_diff=0.015,
+    pair_cor_max=0.7,
+    dE=0.02,
+    mic=False,
+)
 
 pairing = CutAndSplicePairing(slab, n_to_optimize, blmin)
-mutations = OperationSelector([1., 1., 1.],
-                              [MirrorMutation(blmin, n_to_optimize),
-                               RattleMutation(blmin, n_to_optimize),
-                               PermutationMutation(n_to_optimize)])
+mutations = OperationSelector(
+    [1.0, 1.0, 1.0],
+    [
+        MirrorMutation(blmin, n_to_optimize),
+        RattleMutation(blmin, n_to_optimize),
+        PermutationMutation(n_to_optimize),
+    ],
+)
 
 # Relax all unrelaxed structures (e.g. the starting population)
 while da.get_number_of_unrelaxed_candidates() > 0:
@@ -52,9 +57,9 @@ while da.get_number_of_unrelaxed_candida
     da.add_relaxed_step(a)
 
 # create the population
-population = Population(data_connection=da,
-                        population_size=population_size,
-                        comparator=comp)
+population = Population(
+    data_connection=da, population_size=population_size, comparator=comp
+)
 
 # test n_to_test new candidates
 for i in range(n_to_test):
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_basic_parallel_main.py 3.26.0-1/doc/tutorials/ga/ga_basic_parallel_main.py
--- 3.24.0-1/doc/tutorials/ga/ga_basic_parallel_main.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_basic_parallel_main.py	2025-08-12 11:26:23.000000000 +0000
@@ -25,28 +25,32 @@ da = DataConnection('gadb.db')
 tmp_folder = 'tmp_folder/'
 
 # An extra object is needed to handle the parallel execution
-parallel_local_run = ParallelLocalRun(data_connection=da,
-                                      tmp_folder=tmp_folder,
-                                      n_simul=4,
-                                      calc_script='calc.py')
+parallel_local_run = ParallelLocalRun(
+    data_connection=da, tmp_folder=tmp_folder, n_simul=4, calc_script='calc.py'
+)
 
 atom_numbers_to_optimize = da.get_atom_numbers_to_optimize()
 n_to_optimize = len(atom_numbers_to_optimize)
 slab = da.get_slab()
 all_atom_types = get_all_atom_types(slab, atom_numbers_to_optimize)
-blmin = closest_distances_generator(all_atom_types,
-                                    ratio_of_covalent_radii=0.7)
+blmin = closest_distances_generator(all_atom_types, ratio_of_covalent_radii=0.7)
 
-comp = InteratomicDistanceComparator(n_top=n_to_optimize,
-                                     pair_cor_cum_diff=0.015,
-                                     pair_cor_max=0.7,
-                                     dE=0.02,
-                                     mic=False)
+comp = InteratomicDistanceComparator(
+    n_top=n_to_optimize,
+    pair_cor_cum_diff=0.015,
+    pair_cor_max=0.7,
+    dE=0.02,
+    mic=False,
+)
 pairing = CutAndSplicePairing(slab, n_to_optimize, blmin)
-mutations = OperationSelector([1., 1., 1.],
-                              [MirrorMutation(blmin, n_to_optimize),
-                               RattleMutation(blmin, n_to_optimize),
-                               PermutationMutation(n_to_optimize)])
+mutations = OperationSelector(
+    [1.0, 1.0, 1.0],
+    [
+        MirrorMutation(blmin, n_to_optimize),
+        RattleMutation(blmin, n_to_optimize),
+        PermutationMutation(n_to_optimize),
+    ],
+)
 
 # Relax all unrelaxed structures (e.g. the starting population)
 while da.get_number_of_unrelaxed_candidates() > 0:
@@ -55,12 +59,12 @@ while da.get_number_of_unrelaxed_candida
 
 # Wait until the starting population is relaxed
 while parallel_local_run.get_number_of_jobs_running() > 0:
-    time.sleep(5.)
+    time.sleep(5.0)
 
 # create the population
-population = Population(data_connection=da,
-                        population_size=population_size,
-                        comparator=comp)
+population = Population(
+    data_connection=da, population_size=population_size, comparator=comp
+)
 
 # test n_to_test new candidates
 for i in range(n_to_test):
@@ -84,6 +88,6 @@ for i in range(n_to_test):
 
 # Wait until the last candidates are relaxed
 while parallel_local_run.get_number_of_jobs_running() > 0:
-    time.sleep(5.)
+    time.sleep(5.0)
 
 write('all_candidates.traj', da.get_all_relaxed_candidates())
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_basic_parameters.py 3.26.0-1/doc/tutorials/ga/ga_basic_parameters.py
--- 3.24.0-1/doc/tutorials/ga/ga_basic_parameters.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_basic_parameters.py	2025-08-12 11:26:23.000000000 +0000
@@ -42,8 +42,12 @@ def jtg(job_name, traj_file):
 def combine_parameters(conf):
     # Get and combine selected parameters
     parameters = []
-    gets = [get_atoms_connections(conf) + get_rings(conf) +
-            get_angles_distribution(conf) + get_atoms_distribution(conf)]
+    gets = [
+        get_atoms_connections(conf)
+        + get_rings(conf)
+        + get_angles_distribution(conf)
+        + get_atoms_distribution(conf)
+    ]
     for get in gets:
         parameters += get
     return parameters
@@ -70,43 +74,52 @@ mutation_probability = 0.3
 da = DataConnection('gadb.db')
 tmp_folder = 'work_folder/'
 # The PBS queing interface is created
-pbs_run = PBSQueueRun(da,
-                      tmp_folder=tmp_folder,
-                      job_prefix='Ag2Au2_opt',
-                      n_simul=5,
-                      job_template_generator=jtg,
-                      find_neighbors=get_neighborlist,
-                      perform_parametrization=combine_parameters)
+pbs_run = PBSQueueRun(
+    da,
+    tmp_folder=tmp_folder,
+    job_prefix='Ag2Au2_opt',
+    n_simul=5,
+    job_template_generator=jtg,
+    find_neighbors=get_neighborlist,
+    perform_parametrization=combine_parameters,
+)
 
 atom_numbers_to_optimize = da.get_atom_numbers_to_optimize()
 n_to_optimize = len(atom_numbers_to_optimize)
 slab = da.get_slab()
 all_atom_types = get_all_atom_types(slab, atom_numbers_to_optimize)
-blmin = closest_distances_generator(all_atom_types,
-                                    ratio_of_covalent_radii=0.7)
+blmin = closest_distances_generator(all_atom_types, ratio_of_covalent_radii=0.7)
 
-comp = InteratomicDistanceComparator(n_top=n_to_optimize,
-                                     pair_cor_cum_diff=0.015,
-                                     pair_cor_max=0.7,
-                                     dE=0.02,
-                                     mic=False)
+comp = InteratomicDistanceComparator(
+    n_top=n_to_optimize,
+    pair_cor_cum_diff=0.015,
+    pair_cor_max=0.7,
+    dE=0.02,
+    mic=False,
+)
 pairing = CutAndSplicePairing(slab, n_to_optimize, blmin)
-mutations = OperationSelector([1., 1., 1.],
-                              [MirrorMutation(blmin, n_to_optimize),
-                               RattleMutation(blmin, n_to_optimize),
-                               PermutationMutation(n_to_optimize)])
+mutations = OperationSelector(
+    [1.0, 1.0, 1.0],
+    [
+        MirrorMutation(blmin, n_to_optimize),
+        RattleMutation(blmin, n_to_optimize),
+        PermutationMutation(n_to_optimize),
+    ],
+)
 
 # Relax all unrelaxed structures (e.g. the starting population)
-while (da.get_number_of_unrelaxed_candidates() > 0 and
-       not pbs_run.enough_jobs_running()):
+while (
+    da.get_number_of_unrelaxed_candidates() > 0
+    and not pbs_run.enough_jobs_running()
+):
     a = da.get_an_unrelaxed_candidate()
     pbs_run.relax(a)
 
 
 # create the population
-population = Population(data_connection=da,
-                        population_size=population_size,
-                        comparator=comp)
+population = Population(
+    data_connection=da, population_size=population_size, comparator=comp
+)
 
 # create the regression expression for estimating the energy
 all_trajs = da.get_all_relaxed_candidates()
@@ -127,8 +140,10 @@ else:
     weights = None
 
 # Submit new candidates until enough are running
-while (not pbs_run.enough_jobs_running() and
-       len(population.get_current_population()) > 2):
+while (
+    not pbs_run.enough_jobs_running()
+    and len(population.get_current_population()) > 2
+):
     a1, a2 = population.get_two_candidates()
 
     # Selecting the "worst" parent energy
@@ -146,8 +161,9 @@ while (not pbs_run.enough_jobs_running()
 
     if random() < mutation_probability:
         a3_mut, desc_mut = mutations.get_new_individual([a3])
-        if (a3_mut is not None and
-                not should_we_skip(a3_mut, comparison_energy, weights)):
+        if a3_mut is not None and not should_we_skip(
+            a3_mut, comparison_energy, weights
+        ):
             da.add_unrelaxed_step(a3_mut, desc_mut)
             a3 = a3_mut
     pbs_run.relax(a3)
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_basic_pbs_main.py 3.26.0-1/doc/tutorials/ga/ga_basic_pbs_main.py
--- 3.24.0-1/doc/tutorials/ga/ga_basic_pbs_main.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_basic_pbs_main.py	2025-08-12 11:26:23.000000000 +0000
@@ -33,44 +33,55 @@ mutation_probability = 0.3
 da = DataConnection('gadb.db')
 tmp_folder = 'tmp_folder/'
 # The PBS queing interface is created
-pbs_run = PBSQueueRun(da,
-                      tmp_folder=tmp_folder,
-                      job_prefix='Ag2Au2_opt',
-                      n_simul=5,
-                      job_template_generator=jtg)
+pbs_run = PBSQueueRun(
+    da,
+    tmp_folder=tmp_folder,
+    job_prefix='Ag2Au2_opt',
+    n_simul=5,
+    job_template_generator=jtg,
+)
 
 atom_numbers_to_optimize = da.get_atom_numbers_to_optimize()
 n_to_optimize = len(atom_numbers_to_optimize)
 slab = da.get_slab()
 all_atom_types = get_all_atom_types(slab, atom_numbers_to_optimize)
-blmin = closest_distances_generator(all_atom_types,
-                                    ratio_of_covalent_radii=0.7)
+blmin = closest_distances_generator(all_atom_types, ratio_of_covalent_radii=0.7)
 
-comp = InteratomicDistanceComparator(n_top=n_to_optimize,
-                                     pair_cor_cum_diff=0.015,
-                                     pair_cor_max=0.7,
-                                     dE=0.02,
-                                     mic=False)
+comp = InteratomicDistanceComparator(
+    n_top=n_to_optimize,
+    pair_cor_cum_diff=0.015,
+    pair_cor_max=0.7,
+    dE=0.02,
+    mic=False,
+)
 pairing = CutAndSplicePairing(slab, n_to_optimize, blmin)
-mutations = OperationSelector([1., 1., 1.],
-                              [MirrorMutation(blmin, n_to_optimize),
-                               RattleMutation(blmin, n_to_optimize),
-                               PermutationMutation(n_to_optimize)])
+mutations = OperationSelector(
+    [1.0, 1.0, 1.0],
+    [
+        MirrorMutation(blmin, n_to_optimize),
+        RattleMutation(blmin, n_to_optimize),
+        PermutationMutation(n_to_optimize),
+    ],
+)
 
 # Relax all unrelaxed structures (e.g. the starting population)
-while (da.get_number_of_unrelaxed_candidates() > 0 and
-       not pbs_run.enough_jobs_running()):
+while (
+    da.get_number_of_unrelaxed_candidates() > 0
+    and not pbs_run.enough_jobs_running()
+):
     a = da.get_an_unrelaxed_candidate()
     pbs_run.relax(a)
 
 # create the population
-population = Population(data_connection=da,
-                        population_size=population_size,
-                        comparator=comp)
+population = Population(
+    data_connection=da, population_size=population_size, comparator=comp
+)
 
 # Submit new candidates until enough are running
-while (not pbs_run.enough_jobs_running() and
-       len(population.get_current_population()) > 2):
+while (
+    not pbs_run.enough_jobs_running()
+    and len(population.get_current_population()) > 2
+):
     a1, a2 = population.get_two_candidates()
     a3, desc = pairing.get_new_individual([a1, a2])
     if a3 is None:
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_bulk_relax.py 3.26.0-1/doc/tutorials/ga/ga_bulk_relax.py
--- 3.24.0-1/doc/tutorials/ga/ga_bulk_relax.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_bulk_relax.py	2025-08-12 11:26:23.000000000 +0000
@@ -14,8 +14,9 @@ def finalize(atoms, energy=None, forces=
     # Finalizes the atoms by attaching a SinglePointCalculator
     # and setting the raw score as the negative of the total energy
     atoms.wrap()
-    calc = SinglePointCalculator(atoms, energy=energy, forces=forces,
-                                 stress=stress)
+    calc = SinglePointCalculator(
+        atoms, energy=energy, forces=forces, stress=stress
+    )
     atoms.calc = calc
     raw_score = -atoms.get_potential_energy()
     set_raw_score(atoms, raw_score)
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_bulk_run.py 3.26.0-1/doc/tutorials/ga/ga_bulk_run.py
--- 3.24.0-1/doc/tutorials/ga/ga_bulk_run.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_bulk_run.py	2025-08-12 11:26:23.000000000 +0000
@@ -19,31 +19,57 @@ n_top = len(atom_numbers_to_optimize)
 
 # Use Oganov's fingerprint functions to decide whether
 # two structures are identical or not
-comp = OFPComparator(n_top=n_top, dE=1.0,
-                     cos_dist_max=1e-3, rcut=10., binwidth=0.05,
-                     pbc=[True, True, True], sigma=0.05, nsigma=4,
-                     recalculate=False)
+comp = OFPComparator(
+    n_top=n_top,
+    dE=1.0,
+    cos_dist_max=1e-3,
+    rcut=10.0,
+    binwidth=0.05,
+    pbc=[True, True, True],
+    sigma=0.05,
+    nsigma=4,
+    recalculate=False,
+)
 
 # Define the cell and interatomic distance bounds
 # that the candidates must obey
 blmin = closest_distances_generator(atom_numbers_to_optimize, 0.5)
 
-cellbounds = CellBounds(bounds={'phi': [20, 160], 'chi': [20, 160],
-                                'psi': [20, 160], 'a': [2, 60],
-                                'b': [2, 60], 'c': [2, 60]})
+cellbounds = CellBounds(
+    bounds={
+        'phi': [20, 160],
+        'chi': [20, 160],
+        'psi': [20, 160],
+        'a': [2, 60],
+        'b': [2, 60],
+        'c': [2, 60],
+    }
+)
 
 # Define a pairing operator with 100% (0%) chance that the first
 # (second) parent will be randomly translated, and with each parent
 # contributing to at least 15% of the child's scaled coordinates
-pairing = CutAndSplicePairing(slab, n_top, blmin, p1=1., p2=0., minfrac=0.15,
-                              number_of_variable_cell_vectors=3,
-                              cellbounds=cellbounds, use_tags=False)
+pairing = CutAndSplicePairing(
+    slab,
+    n_top,
+    blmin,
+    p1=1.0,
+    p2=0.0,
+    minfrac=0.15,
+    number_of_variable_cell_vectors=3,
+    cellbounds=cellbounds,
+    use_tags=False,
+)
 
 # Define a strain mutation with a typical standard deviation of 0.7
 # for the strain matrix elements (drawn from a normal distribution)
-strainmut = StrainMutation(blmin, stddev=0.7, cellbounds=cellbounds,
-                           number_of_variable_cell_vectors=3,
-                           use_tags=False)
+strainmut = StrainMutation(
+    blmin,
+    stddev=0.7,
+    cellbounds=cellbounds,
+    number_of_variable_cell_vectors=3,
+    use_tags=False,
+)
 
 # Define a soft mutation; we need to provide a dictionary with
 # (typically rather short) minimal interatomic distances which
@@ -52,15 +78,14 @@ strainmut = StrainMutation(blmin, stddev
 # distances (in Angstrom) for a valid mutation are provided via
 # the 'bounds' keyword argument.
 blmin_soft = closest_distances_generator(atom_numbers_to_optimize, 0.1)
-softmut = SoftMutation(blmin_soft, bounds=[2., 5.], use_tags=False)
+softmut = SoftMutation(blmin_soft, bounds=[2.0, 5.0], use_tags=False)
 # By default, the operator will update a "used_modes.json" file
 # after every mutation, listing which modes have been used so far
 # for each structure in the database. The mode indices start at 3
 # as the three lowest frequency modes are translational modes.
 
 # Set up the relative probabilities for the different operators
-operators = OperationSelector([4., 3., 3.],
-                              [pairing, softmut, strainmut])
+operators = OperationSelector([4.0, 3.0, 3.0], [pairing, softmut, strainmut])
 
 # Relax the initial candidates
 while da.get_number_of_unrelaxed_candidates() > 0:
@@ -75,11 +100,13 @@ while da.get_number_of_unrelaxed_candida
 
 # Initialize the population
 population_size = 20
-population = Population(data_connection=da,
-                        population_size=population_size,
-                        comparator=comp,
-                        logfile='log.txt',
-                        use_extinct=True)
+population = Population(
+    data_connection=da,
+    population_size=population_size,
+    comparator=comp,
+    logfile='log.txt',
+    use_extinct=True,
+)
 
 # Update the scaling volume used in some operators
 # based on a number of the best candidates
@@ -123,8 +150,7 @@ for step in range(n_to_test):
         # and the pairing operator based on the current
         # best structures contained in the population
         current_pop = population.get_current_population()
-        strainmut.update_scaling_volume(current_pop, w_adapt=0.5,
-                                        n_adapt=4)
+        strainmut.update_scaling_volume(current_pop, w_adapt=0.5, n_adapt=4)
         pairing.update_scaling_volume(current_pop, w_adapt=0.5, n_adapt=4)
         write('current_population.traj', current_pop)
 
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_bulk_start.py 3.26.0-1/doc/tutorials/ga/ga_bulk_start.py
--- 3.24.0-1/doc/tutorials/ga/ga_bulk_start.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_bulk_start.py	2025-08-12 11:26:23.000000000 +0000
@@ -8,7 +8,7 @@ from ase.ga.utilities import CellBounds,
 N = 20
 
 # Target cell volume for the initial structures, in angstrom^3
-volume = 240.
+volume = 240.0
 
 # Specify the 'building blocks' from which the initial structures
 # will be constructed. Here we take single Ag atoms as building
@@ -19,14 +19,22 @@ blocks = ['Ag'] * 24
 
 # Generate a dictionary with the closest allowed interatomic distances
 Z = atomic_numbers['Ag']
-blmin = closest_distances_generator(atom_numbers=[Z],
-                                    ratio_of_covalent_radii=0.5)
+blmin = closest_distances_generator(
+    atom_numbers=[Z], ratio_of_covalent_radii=0.5
+)
 
 # Specify reasonable bounds on the minimal and maximal
 # cell vector lengths (in angstrom) and angles (in degrees)
-cellbounds = CellBounds(bounds={'phi': [35, 145], 'chi': [35, 145],
-                                'psi': [35, 145], 'a': [3, 50],
-                                'b': [3, 50], 'c': [3, 50]})
+cellbounds = CellBounds(
+    bounds={
+        'phi': [35, 145],
+        'chi': [35, 145],
+        'psi': [35, 145],
+        'a': [3, 50],
+        'b': [3, 50],
+        'c': [3, 50],
+    }
+)
 
 # Choose an (optional) 'cell splitting' scheme which basically
 # controls the level of translational symmetry (within the unit
@@ -46,13 +54,18 @@ splits = {(2,): 1, (1,): 1}
 slab = Atoms('', pbc=True)
 
 # Initialize the random structure generator
-sg = StartGenerator(slab, blocks, blmin, box_volume=volume,
-                    number_of_variable_cell_vectors=3,
-                    cellbounds=cellbounds, splits=splits)
+sg = StartGenerator(
+    slab,
+    blocks,
+    blmin,
+    box_volume=volume,
+    number_of_variable_cell_vectors=3,
+    cellbounds=cellbounds,
+    splits=splits,
+)
 
 # Create the database
-da = PrepareDB(db_file_name='gadb.db',
-               stoichiometry=[Z] * 24)
+da = PrepareDB(db_file_name='gadb.db', stoichiometry=[Z] * 24)
 
 # Generate N random structures
 # and add them to the database
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_convex_run.py 3.26.0-1/doc/tutorials/ga/ga_convex_run.py
--- 3.24.0-1/doc/tutorials/ga/ga_convex_run.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_convex_run.py	2025-08-12 11:26:23.000000000 +0000
@@ -41,7 +41,7 @@ def get_mixing_energy(atoms):
 
 
 def get_avg_lattice_constant(syms):
-    a = 0.
+    a = 0.0
     for m in set(syms):
         a += syms.count(m) * lattice_constants[m]
     return a / len(syms)
@@ -58,18 +58,19 @@ num_gens = 10
 # how often each is picked on average
 # The probability for an operator is the prepended integer divided by the sum
 # of integers
-oclist = [(3, CutSpliceSlabCrossover()),
-          (1, RandomSlabPermutation()),
-          (1, RandomCompositionMutation())
-          ]
+oclist = [
+    (3, CutSpliceSlabCrossover()),
+    (1, RandomSlabPermutation()),
+    (1, RandomCompositionMutation()),
+]
 operation_selector = OperationSelector(*zip(*oclist))
 
 # Pass parameters to the population instance
 # A variable_function is required to divide candidates into groups here we use
 # the chemical composition
-pop = RankFitnessPopulation(data_connection=db,
-                            population_size=pop_size,
-                            variable_function=get_comp)
+pop = RankFitnessPopulation(
+    data_connection=db, population_size=pop_size, variable_function=get_comp
+)
 
 # Evaluate the starting population
 # The only requirement of the evaluation is to set the raw_score
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_convex_start.py 3.26.0-1/doc/tutorials/ga/ga_convex_start.py
--- 3.24.0-1/doc/tutorials/ga/ga_convex_start.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_convex_start.py	2025-08-12 11:26:23.000000000 +0000
@@ -9,7 +9,7 @@ from ase.ga.data import PrepareDB
 
 
 def get_avg_lattice_constant(syms):
-    a = 0.
+    a = 0.0
     for m in set(syms):
         a += syms.count(m) * lattice_constants[m]
     return a / len(syms)
@@ -17,16 +17,18 @@ def get_avg_lattice_constant(syms):
 
 metals = ['Cu', 'Pt']
 # Use experimental lattice constants
-lattice_constants = {m: reference_states[atomic_numbers[m]]['a']
-                     for m in metals}
+lattice_constants = {
+    m: reference_states[atomic_numbers[m]]['a'] for m in metals
+}
 
 # Create the references (pure slabs) manually
 pure_slabs = []
 refs = {}
 print('Reference energies:')
 for m in metals:
-    slab = fcc111(m, size=(2, 4, 3), a=lattice_constants[m],
-                  vacuum=5, orthogonal=True)
+    slab = fcc111(
+        m, size=(2, 4, 3), a=lattice_constants[m], vacuum=5, orthogonal=True
+    )
     slab.calc = EMT()
 
     # We save the reference energy as E_A / N
@@ -46,15 +48,20 @@ pop_size = 2 * len(slab)
 target = Path('hull.db')
 if target.exists():
     target.unlink()
-db = PrepareDB(target, population_size=pop_size,
-               reference_energies=refs, metals=metals,
-               lattice_constants=lattice_constants)
+db = PrepareDB(
+    target,
+    population_size=pop_size,
+    reference_energies=refs,
+    metals=metals,
+    lattice_constants=lattice_constants,
+)
 
 # We add the pure slabs to the database as relaxed because we have already
 # set the raw_score
 for slab in pure_slabs:
-    db.add_relaxed_candidate(slab,
-                             atoms_string=''.join(slab.get_chemical_symbols()))
+    db.add_relaxed_candidate(
+        slab, atoms_string=''.join(slab.get_chemical_symbols())
+    )
 
 
 # Now we create the rest of the candidates for the initial population
@@ -66,9 +73,13 @@ for i in range(pop_size - 2):
     symbols = [metals[0]] * nA + [metals[1]] * nB + metals
 
     # Making a generic slab with the correct lattice constant
-    slab = fcc111('X', size=(2, 4, 3),
-                  a=get_avg_lattice_constant(symbols),
-                  vacuum=5, orthogonal=True)
+    slab = fcc111(
+        'X',
+        size=(2, 4, 3),
+        a=get_avg_lattice_constant(symbols),
+        vacuum=5,
+        orthogonal=True,
+    )
 
     # Setting the symbols and randomizing the order
     slab.set_chemical_symbols(symbols)
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_fcc_alloys_main.py 3.26.0-1/doc/tutorials/ga/ga_fcc_alloys_main.py
--- 3.24.0-1/doc/tutorials/ga/ga_fcc_alloys_main.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_fcc_alloys_main.py	2025-08-12 11:26:23.000000000 +0000
@@ -20,13 +20,14 @@ metals = db.get_param('metals')
 # Specify the procreation operators for the algorithm
 # Try and play with the mutation operators that move to nearby
 # places in the periodic table
-oclist = ([1, 1], [RandomElementMutation(metals),
-                   OnePointElementCrossover(metals)])
+oclist = (
+    [1, 1],
+    [RandomElementMutation(metals), OnePointElementCrossover(metals)],
+)
 operation_selector = OperationSelector(*oclist)
 
 # Pass parameters to the population instance
-pop = Population(data_connection=db,
-                 population_size=population_size)
+pop = Population(data_connection=db, population_size=population_size)
 
 # We form generations in this algorithm run and can therefore set
 # a convergence criteria based on generations
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_fcc_alloys_relax.py 3.26.0-1/doc/tutorials/ga/ga_fcc_alloys_relax.py
--- 3.24.0-1/doc/tutorials/ga/ga_fcc_alloys_relax.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_fcc_alloys_relax.py	2025-08-12 11:26:23.000000000 +0000
@@ -13,7 +13,7 @@ def relax(input_atoms, ref_db):
     db = connect(ref_db)
 
     # Load our model structure which is just FCC
-    atoms = FaceCenteredCubic('X', latticeconstant=1.)
+    atoms = FaceCenteredCubic('X', latticeconstant=1.0)
     atoms.set_chemical_symbols(atoms_string)
 
     # Compute the average lattice constant of the metals in this individual
@@ -33,15 +33,15 @@ def relax(input_atoms, ref_db):
     # relaxation
     atoms.calc = EMT()
     eps = 0.05
-    volumes = (a * np.linspace(1 - eps, 1 + eps, 9))**3
+    volumes = (a * np.linspace(1 - eps, 1 + eps, 9)) ** 3
     energies = []
     for v in volumes:
-        atoms.set_cell([v**(1. / 3)] * 3, scale_atoms=True)
+        atoms.set_cell([v ** (1.0 / 3)] * 3, scale_atoms=True)
         energies.append(atoms.get_potential_energy())
 
     eos = EquationOfState(volumes, energies)
     v1, ef, _B = eos.fit()
-    latticeconstant = v1**(1. / 3)
+    latticeconstant = v1 ** (1.0 / 3)
 
     # Calculate the heat of formation by subtracting ef with ei
     hof = (ef - ei) / len(atoms)
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_fcc_alloys_start.py 3.26.0-1/doc/tutorials/ga/ga_fcc_alloys_start.py
--- 3.24.0-1/doc/tutorials/ga/ga_fcc_alloys_start.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_fcc_alloys_start.py	2025-08-12 11:26:23.000000000 +0000
@@ -8,12 +8,11 @@ metals = ['Al', 'Au', 'Cu', 'Ag', 'Pd',
 population_size = 10
 
 # Create database
-db = PrepareDB('fcc_alloys.db',
-               population_size=population_size,
-               metals=metals)
+db = PrepareDB('fcc_alloys.db', population_size=population_size, metals=metals)
 
 # Create starting population
 for i in range(population_size):
     atoms_string = [random.choice(metals) for _ in range(4)]
-    db.add_unrelaxed_candidate(Atoms(atoms_string),
-                               atoms_string=''.join(atoms_string))
+    db.add_unrelaxed_candidate(
+        Atoms(atoms_string), atoms_string=''.join(atoms_string)
+    )
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_fcc_references.py 3.26.0-1/doc/tutorials/ga/ga_fcc_references.py
--- 3.24.0-1/doc/tutorials/ga/ga_fcc_references.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_fcc_references.py	2025-08-12 11:26:23.000000000 +0000
@@ -15,18 +15,21 @@ for m in metals:
     a = atoms.cell[0][0]
 
     eps = 0.05
-    volumes = (a * np.linspace(1 - eps, 1 + eps, 9))**3
+    volumes = (a * np.linspace(1 - eps, 1 + eps, 9)) ** 3
     energies = []
     for v in volumes:
-        atoms.set_cell([v**(1. / 3)] * 3, scale_atoms=True)
+        atoms.set_cell([v ** (1.0 / 3)] * 3, scale_atoms=True)
         energies.append(atoms.get_potential_energy())
 
     eos = EquationOfState(volumes, energies)
     v1, e1, B = eos.fit()
 
-    atoms.set_cell([v1**(1. / 3)] * 3, scale_atoms=True)
+    atoms.set_cell([v1 ** (1.0 / 3)] * 3, scale_atoms=True)
     ef = atoms.get_potential_energy()
 
-    db.write(atoms, metal=m,
-             latticeconstant=v1**(1. / 3),
-             energy_per_atom=ef / len(atoms))
+    db.write(
+        atoms,
+        metal=m,
+        latticeconstant=v1 ** (1.0 / 3),
+        energy_per_atom=ef / len(atoms),
+    )
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_molecular_crystal_relax.py 3.26.0-1/doc/tutorials/ga/ga_molecular_crystal_relax.py
--- 3.24.0-1/doc/tutorials/ga/ga_molecular_crystal_relax.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_molecular_crystal_relax.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,5 @@
 """Tools for locally structure optimization."""
+
 import os
 from time import time
 
@@ -21,12 +22,19 @@ def relax(atoms):
     # "Phase Diagrams of Diatomic Molecules:
     #  Using the Gibbs Ensemble Monte Carlo Method",
     # Molecular Simulations, 13, 11 (1994).
-    calc = HarmonicPlusLennardJones(epsilon=37.3 * kB, sigma=3.31, rc=12.,
-                                    r0=1.12998, k=10.)
+    calc = HarmonicPlusLennardJones(
+        epsilon=37.3 * kB, sigma=3.31, rc=12.0, r0=1.12998, k=10.0
+    )
     atoms.calc = calc
 
-    dyn = PreconLBFGS(atoms, variable_cell=True, maxstep=0.2,
-                      use_armijo=True, logfile='opt.log', trajectory='opt.traj')
+    dyn = PreconLBFGS(
+        atoms,
+        variable_cell=True,
+        maxstep=0.2,
+        use_armijo=True,
+        logfile='opt.log',
+        trajectory='opt.traj',
+    )
     dyn.run(fmax=3e-2, smax=5e-4, steps=250)
 
     e = atoms.get_potential_energy()
@@ -44,8 +52,9 @@ def relax(atoms):
 def finalize(atoms, energy=None, forces=None, stress=None):
     niggli_reduce(atoms)
     atoms.wrap()
-    calc = SinglePointCalculator(atoms, energy=energy, forces=forces,
-                                 stress=stress)
+    calc = SinglePointCalculator(
+        atoms, energy=energy, forces=forces, stress=stress
+    )
     atoms.calc = calc
     raw_score = -atoms.get_potential_energy()
     set_raw_score(atoms, raw_score)
@@ -59,6 +68,7 @@ class HarmonicPlusLennardJones(LennardJo
     Only works for structures consisting of a series
     of molecular dimers and with only one element.
     """
+
     implemented_properties = ['energy', 'forces', 'stress']
     default_parameters = {'k': 1.0, 'r0': 1.0}
     nolabel = True
@@ -66,9 +76,9 @@ class HarmonicPlusLennardJones(LennardJo
     def __init__(self, **kwargs):
         LennardJones.__init__(self, **kwargs)
 
-    def calculate(self, atoms=None,
-                  properties=['energy'],
-                  system_changes=all_changes):
+    def calculate(
+        self, atoms=None, properties=['energy'], system_changes=all_changes
+    ):
         LennardJones.calculate(self, atoms, properties, system_changes)
 
         natoms = len(self.atoms)
@@ -100,9 +110,9 @@ class HarmonicPlusLennardJones(LennardJo
             stress += np.dot(np.array([f]).T, np.array([d]))
 
             # Substracting intramolecular LJ part
-            r2 = r ** 2
-            c6 = (sigma**2 / r2)**3
-            c12 = c6 ** 2
+            r2 = r**2
+            c6 = (sigma**2 / r2) ** 3
+            c12 = c6**2
             energy += -4 * epsilon * (c12 - c6).sum()
             f = (24 * epsilon * (2 * c12 - c6) / r2) * d
             forces[a1] -= -f
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_molecular_crystal_run.py 3.26.0-1/doc/tutorials/ga/ga_molecular_crystal_run.py
--- 3.24.0-1/doc/tutorials/ga/ga_molecular_crystal_run.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_molecular_crystal_run.py	2025-08-12 11:26:23.000000000 +0000
@@ -24,32 +24,44 @@ slab = da.get_slab()
 atom_numbers_to_optimize = da.get_atom_numbers_to_optimize()
 n_top = len(atom_numbers_to_optimize)
 blmin = closest_distances_generator(atom_numbers_to_optimize, 1.0)
-cellbounds = CellBounds(bounds={'phi': [30, 150], 'chi': [30, 150],
-                                'psi': [30, 150]})
+cellbounds = CellBounds(
+    bounds={'phi': [30, 150], 'chi': [30, 150], 'psi': [30, 150]}
+)
 
 # Note the "use_tags" keyword argument being used
 # to signal that we want to preserve molecular identity
 # via the tags
-pairing = CutAndSplicePairing(slab, n_top, blmin, p1=1., p2=0.,
-                              minfrac=0.15, cellbounds=cellbounds,
-                              number_of_variable_cell_vectors=3,
-                              use_tags=True)
-
-rattlemut = RattleMutation(blmin, n_top, rattle_prop=0.3, rattle_strength=0.5,
-                           use_tags=True)
-
-strainmut = StrainMutation(blmin, stddev=0.7, cellbounds=cellbounds,
-                           use_tags=True)
+pairing = CutAndSplicePairing(
+    slab,
+    n_top,
+    blmin,
+    p1=1.0,
+    p2=0.0,
+    minfrac=0.15,
+    cellbounds=cellbounds,
+    number_of_variable_cell_vectors=3,
+    use_tags=True,
+)
+
+rattlemut = RattleMutation(
+    blmin, n_top, rattle_prop=0.3, rattle_strength=0.5, use_tags=True
+)
+
+strainmut = StrainMutation(
+    blmin, stddev=0.7, cellbounds=cellbounds, use_tags=True
+)
 
 rotmut = RotationalMutation(blmin, fraction=0.3, min_angle=0.5 * np.pi)
 
 rattlerotmut = RattleRotationalMutation(rattlemut, rotmut)
 
 blmin_soft = closest_distances_generator(atom_numbers_to_optimize, 0.8)
-softmut = SoftMutation(blmin_soft, bounds=[2., 5.], use_tags=True)
+softmut = SoftMutation(blmin_soft, bounds=[2.0, 5.0], use_tags=True)
 
-operators = OperationSelector([5, 1, 1, 1, 1, 1], [
-    pairing, rattlemut, strainmut, rotmut, rattlerotmut, softmut])
+operators = OperationSelector(
+    [5, 1, 1, 1, 1, 1],
+    [pairing, rattlemut, strainmut, rotmut, rattlerotmut, softmut],
+)
 
 # Relaxing the initial candidates
 while da.get_number_of_unrelaxed_candidates() > 0:
@@ -58,15 +70,22 @@ while da.get_number_of_unrelaxed_candida
     da.add_relaxed_step(a)
 
 # The structure comparator for the population
-comp = OFPComparator(n_top=n_top, dE=1.0, cos_dist_max=5e-3, rcut=10.,
-                     binwidth=0.05, pbc=[True, True, True], sigma=0.05,
-                     nsigma=4, recalculate=False)
+comp = OFPComparator(
+    n_top=n_top,
+    dE=1.0,
+    cos_dist_max=5e-3,
+    rcut=10.0,
+    binwidth=0.05,
+    pbc=[True, True, True],
+    sigma=0.05,
+    nsigma=4,
+    recalculate=False,
+)
 
 # The population
-population = Population(data_connection=da,
-                        population_size=10,
-                        comparator=comp,
-                        logfile='log.txt')
+population = Population(
+    data_connection=da, population_size=10, comparator=comp, logfile='log.txt'
+)
 
 current_pop = population.get_current_population()
 strainmut.update_scaling_volume(current_pop, w_adapt=0.5, n_adapt=4)
@@ -96,16 +115,19 @@ for step in range(n_to_test):
 
     # Update the strain mutation and pairing operators
     if step % 10 == 0:
-        strainmut.update_scaling_volume(current_pop, w_adapt=0.5,
-                                        n_adapt=4)
+        strainmut.update_scaling_volume(current_pop, w_adapt=0.5, n_adapt=4)
         pairing.update_scaling_volume(current_pop, w_adapt=0.5, n_adapt=4)
 
     # Print out information for easier follow-up/analysis/plotting:
-    print('Step %d %s %.3f %.3f %.3f' % (
-        step, desc, get_raw_score(a1), get_raw_score(a2), get_raw_score(a3)))
-
-    print('Step %d highest raw score in pop: %.3f' %
-          (step, get_raw_score(current_pop[0])))
+    print(
+        'Step %d %s %.3f %.3f %.3f'
+        % (step, desc, get_raw_score(a1), get_raw_score(a2), get_raw_score(a3))
+    )
+
+    print(
+        'Step %d highest raw score in pop: %.3f'
+        % (step, get_raw_score(current_pop[0]))
+    )
 
 print('GA finished after step %d' % step)
 hiscore = get_raw_score(current_pop[0])
diff -pruN 3.24.0-1/doc/tutorials/ga/ga_molecular_crystal_start.py 3.26.0-1/doc/tutorials/ga/ga_molecular_crystal_start.py
--- 3.24.0-1/doc/tutorials/ga/ga_molecular_crystal_start.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/ga_molecular_crystal_start.py	2025-08-12 11:26:23.000000000 +0000
@@ -13,7 +13,7 @@ blocks = [('N2', 8)]
 # get the N2 geometry using ase.build.molecule.
 
 # A guess for the cell volume in Angstrom^3
-box_volume = 30. * 8
+box_volume = 30.0 * 8
 
 # The cell splitting scheme:
 splits = {(2,): 1, (1,): 1}
@@ -24,13 +24,21 @@ splits = {(2,): 1, (1,): 1}
 # distances will only be applied intermolecularly
 # (and not intramolecularly):
 Z = atomic_numbers['N']
-blmin = closest_distances_generator(atom_numbers=[Z],
-                                    ratio_of_covalent_radii=1.3)
+blmin = closest_distances_generator(
+    atom_numbers=[Z], ratio_of_covalent_radii=1.3
+)
 
 # The bounds for the randomly generated unit cells:
-cellbounds = CellBounds(bounds={'phi': [30, 150], 'chi': [30, 150],
-                                'psi': [30, 150], 'a': [3, 50],
-                                'b': [3, 50], 'c': [3, 50]})
+cellbounds = CellBounds(
+    bounds={
+        'phi': [30, 150],
+        'chi': [30, 150],
+        'psi': [30, 150],
+        'a': [3, 50],
+        'b': [3, 50],
+        'c': [3, 50],
+    }
+)
 
 # The familiar 'slab' object, here only providing
 # the PBC as there are no atoms or cell vectors
@@ -38,14 +46,21 @@ cellbounds = CellBounds(bounds={'phi': [
 slab = Atoms('', pbc=True)
 
 # create the starting population
-sg = StartGenerator(slab, blocks, blmin, box_volume=box_volume,
-                    cellbounds=cellbounds, splits=splits,
-                    number_of_variable_cell_vectors=3,
-                    test_too_far=False)
+sg = StartGenerator(
+    slab,
+    blocks,
+    blmin,
+    box_volume=box_volume,
+    cellbounds=cellbounds,
+    splits=splits,
+    number_of_variable_cell_vectors=3,
+    test_too_far=False,
+)
 
 # Initialize the database
-da = PrepareDB(db_file_name='gadb.db', simulation_cell=slab,
-               stoichiometry=[Z] * 16)
+da = PrepareDB(
+    db_file_name='gadb.db', simulation_cell=slab, stoichiometry=[Z] * 16
+)
 
 # Generate the new structures
 # and add them to the database
diff -pruN 3.24.0-1/doc/tutorials/ga/plot_convex_hull.py 3.26.0-1/doc/tutorials/ga/plot_convex_hull.py
--- 3.24.0-1/doc/tutorials/ga/plot_convex_hull.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/ga/plot_convex_hull.py	2025-08-12 11:26:23.000000000 +0000
@@ -15,8 +15,10 @@ for dct in dcts:
     refs.append((dct.formula, -dct.raw_score))
 
 pd = PhaseDiagram(refs)
-ax = pd.plot(show=not True,  # set to True to show plot
-             only_label_simplices=True)
+ax = pd.plot(
+    show=not True,  # set to True to show plot
+    only_label_simplices=True,
+)
 plt.savefig('hull.png')
 
 # View the simplices of the convex hull
diff -pruN 3.24.0-1/doc/tutorials/lattice_constant.py 3.26.0-1/doc/tutorials/lattice_constant.py
--- 3.24.0-1/doc/tutorials/lattice_constant.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/lattice_constant.py	2025-08-12 11:26:23.000000000 +0000
@@ -38,8 +38,7 @@ p = np.linalg.lstsq(functions.T, energie
 # --- literalinclude division line 5 ---
 p0 = p[0]
 p1 = p[1:3]
-p2 = np.array([(2 * p[3], p[4]),
-               (p[4], 2 * p[5])])
+p2 = np.array([(2 * p[3], p[4]), (p[4], 2 * p[5])])
 a0, c0 = np.linalg.solve(p2.T, -p1)
 
 with open('lattice_constant.csv', 'w') as fd:
diff -pruN 3.24.0-1/doc/tutorials/md/moldyn1.py 3.26.0-1/doc/tutorials/md/moldyn1.py
--- 3.24.0-1/doc/tutorials/md/moldyn1.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/md/moldyn1.py	2025-08-12 11:26:23.000000000 +0000
@@ -10,16 +10,20 @@ use_asap = False
 
 if use_asap:
     from asap3 import EMT
+
     size = 10
 else:
     from ase.calculators.emt import EMT
+
     size = 3
 
 # Set up a crystal
-atoms = FaceCenteredCubic(directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
-                          symbol='Cu',
-                          size=(size, size, size),
-                          pbc=True)
+atoms = FaceCenteredCubic(
+    directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
+    symbol='Cu',
+    size=(size, size, size),
+    pbc=True,
+)
 
 # Describe the interatomic interactions with the Effective Medium Theory
 atoms.calc = EMT()
@@ -35,8 +39,10 @@ def printenergy(a):
     """Function to print the potential, kinetic and total energy"""
     epot = a.get_potential_energy() / len(a)
     ekin = a.get_kinetic_energy() / len(a)
-    print('Energy per atom: Epot = %.3feV  Ekin = %.3feV (T=%3.0fK)  '
-          'Etot = %.3feV' % (epot, ekin, ekin / (1.5 * units.kB), epot + ekin))
+    print(
+        'Energy per atom: Epot = %.3feV  Ekin = %.3feV (T=%3.0fK)  '
+        'Etot = %.3feV' % (epot, ekin, ekin / (1.5 * units.kB), epot + ekin)
+    )
 
 
 # Now run the dynamics
diff -pruN 3.24.0-1/doc/tutorials/md/moldyn2.py 3.26.0-1/doc/tutorials/md/moldyn2.py
--- 3.24.0-1/doc/tutorials/md/moldyn2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/md/moldyn2.py	2025-08-12 11:26:23.000000000 +0000
@@ -10,16 +10,20 @@ use_asap = True
 
 if use_asap:
     from asap3 import EMT
+
     size = 10
 else:
     from ase.calculators.emt import EMT
+
     size = 3
 
 # Set up a crystal
-atoms = FaceCenteredCubic(directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
-                          symbol="Cu",
-                          size=(size, size, size),
-                          pbc=True)
+atoms = FaceCenteredCubic(
+    directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
+    symbol='Cu',
+    size=(size, size, size),
+    pbc=True,
+)
 
 # Describe the interatomic interactions with the Effective Medium Theory
 atoms.calc = EMT()
@@ -35,8 +39,10 @@ def printenergy(a=atoms):  # store a ref
     """Function to print the potential, kinetic and total energy."""
     epot = a.get_potential_energy() / len(a)
     ekin = a.get_kinetic_energy() / len(a)
-    print('Energy per atom: Epot = %.3feV  Ekin = %.3feV (T=%3.0fK)  '
-          'Etot = %.3feV' % (epot, ekin, ekin / (1.5 * units.kB), epot + ekin))
+    print(
+        'Energy per atom: Epot = %.3feV  Ekin = %.3feV (T=%3.0fK)  '
+        'Etot = %.3feV' % (epot, ekin, ekin / (1.5 * units.kB), epot + ekin)
+    )
 
 
 # Now run the dynamics
diff -pruN 3.24.0-1/doc/tutorials/md/moldyn3.py 3.26.0-1/doc/tutorials/md/moldyn3.py
--- 3.24.0-1/doc/tutorials/md/moldyn3.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/md/moldyn3.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,5 @@
 """Demonstrates molecular dynamics with constant temperature."""
+
 from asap3 import EMT  # Way too slow with ase.EMT !
 
 from ase import units
@@ -11,10 +12,12 @@ size = 10
 T = 1500  # Kelvin
 
 # Set up a crystal
-atoms = FaceCenteredCubic(directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
-                          symbol="Cu",
-                          size=(size, size, size),
-                          pbc=False)
+atoms = FaceCenteredCubic(
+    directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]],
+    symbol='Cu',
+    size=(size, size, size),
+    pbc=False,
+)
 
 # Describe the interatomic interactions with the Effective Medium Theory
 atoms.calc = EMT()
@@ -29,8 +32,10 @@ def printenergy(a=atoms):  # store a ref
     """Function to print the potential, kinetic and total energy."""
     epot = a.get_potential_energy() / len(a)
     ekin = a.get_kinetic_energy() / len(a)
-    print('Energy per atom: Epot = %.3feV  Ekin = %.3feV (T=%3.0fK)  '
-          'Etot = %.3feV' % (epot, ekin, ekin / (1.5 * units.kB), epot + ekin))
+    print(
+        'Energy per atom: Epot = %.3feV  Ekin = %.3feV (T=%3.0fK)  '
+        'Etot = %.3feV' % (epot, ekin, ekin / (1.5 * units.kB), epot + ekin)
+    )
 
 
 dyn.attach(printenergy, interval=50)
diff -pruN 3.24.0-1/doc/tutorials/md/moldyn4.py 3.26.0-1/doc/tutorials/md/moldyn4.py
--- 3.24.0-1/doc/tutorials/md/moldyn4.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/md/moldyn4.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,5 @@
 """Demonstrates molecular dynamics for isolated particles."""
+
 from ase import units
 from ase.cluster.cubic import FaceCenteredCubic
 from ase.md.velocitydistribution import (
@@ -14,16 +15,20 @@ use_asap = True
 
 if use_asap:
     from asap3 import EMT
+
     size = 4
 else:
     from ase.calculators.emt import EMT
+
     size = 2
 
 # Set up a nanoparticle
-atoms = FaceCenteredCubic('Cu',
-                          surfaces=[[1, 0, 0], [1, 1, 0], [1, 1, 1]],
-                          layers=(size, size, size),
-                          vacuum=4)
+atoms = FaceCenteredCubic(
+    'Cu',
+    surfaces=[[1, 0, 0], [1, 1, 0], [1, 1, 1]],
+    layers=(size, size, size),
+    vacuum=4,
+)
 
 # Describe the interatomic interactions with the Effective Medium Theory
 atoms.calc = EMT()
@@ -47,8 +52,10 @@ def printenergy(a=atoms):  # store a ref
     """Function to print the potential, kinetic and total energy."""
     epot = a.get_potential_energy() / len(a)
     ekin = a.get_kinetic_energy() / len(a)
-    print('Energy per atom: Epot = %.3feV  Ekin = %.3feV (T=%3.0fK)  '
-          'Etot = %.3feV' % (epot, ekin, ekin / (1.5 * units.kB), epot + ekin))
+    print(
+        'Energy per atom: Epot = %.3feV  Ekin = %.3feV (T=%3.0fK)  '
+        'Etot = %.3feV' % (epot, ekin, ekin / (1.5 * units.kB), epot + ekin)
+    )
 
 
 dyn.attach(printenergy, interval=10)
diff -pruN 3.24.0-1/doc/tutorials/minimahopping/Cu2_Pt110.py 3.26.0-1/doc/tutorials/minimahopping/Cu2_Pt110.py
--- 3.24.0-1/doc/tutorials/minimahopping/Cu2_Pt110.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/minimahopping/Cu2_Pt110.py	2025-08-12 11:26:23.000000000 +0000
@@ -5,19 +5,24 @@ from ase.constraints import FixAtoms, Ho
 from ase.optimize.minimahopping import MinimaHopping
 
 # Make the Pt 110 slab.
-atoms = fcc110('Pt', (2, 2, 2), vacuum=7.)
+atoms = fcc110('Pt', (2, 2, 2), vacuum=7.0)
 
 # Add the Cu2 adsorbate.
-adsorbate = Atoms([Atom('Cu', atoms[7].position + (0., 0., 2.5)),
-                   Atom('Cu', atoms[7].position + (0., 0., 5.0))])
+adsorbate = Atoms(
+    [
+        Atom('Cu', atoms[7].position + (0.0, 0.0, 2.5)),
+        Atom('Cu', atoms[7].position + (0.0, 0.0, 5.0)),
+    ]
+)
 atoms.extend(adsorbate)
 
 # Constrain the surface to be fixed and a Hookean constraint between
 # the adsorbate atoms.
-constraints = [FixAtoms(indices=[atom.index for atom in atoms if
-                                 atom.symbol == 'Pt']),
-               Hookean(a1=8, a2=9, rt=2.6, k=15.),
-               Hookean(a1=8, a2=(0., 0., 1., -15.), k=15.), ]
+constraints = [
+    FixAtoms(indices=[atom.index for atom in atoms if atom.symbol == 'Pt']),
+    Hookean(a1=8, a2=9, rt=2.6, k=15.0),
+    Hookean(a1=8, a2=(0.0, 0.0, 1.0, -15.0), k=15.0),
+]
 atoms.set_constraint(constraints)
 
 # Set the calculator.
@@ -25,7 +30,5 @@ calc = EMT()
 atoms.calc = calc
 
 # Instantiate and run the minima hopping algorithm.
-hop = MinimaHopping(atoms,
-                    Ediff0=2.5,
-                    T0=4000.)
+hop = MinimaHopping(atoms, Ediff0=2.5, T0=4000.0)
 hop(totalsteps=10)
diff -pruN 3.24.0-1/doc/tutorials/neb/diffusion.py 3.26.0-1/doc/tutorials/neb/diffusion.py
--- 3.24.0-1/doc/tutorials/neb/diffusion.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/neb/diffusion.py	2025-08-12 11:26:23.000000000 +0000
@@ -16,8 +16,11 @@ for name, a in zip('ITF', images[::2]):
     del a.constraints
     a = a * (2, 2, 1)
     a.set_cell(cell)
-    renderer = write('diffusion-%s.pov' % name, a,
-                     povray_settings=dict(transparent=False, display=False))
+    renderer = write(
+        'diffusion-%s.pov' % name,
+        a,
+        povray_settings=dict(transparent=False, display=False),
+    )
     renderer.render()
 
 nebtools = NEBTools(images)
diff -pruN 3.24.0-1/doc/tutorials/neb/idpp1.py 3.26.0-1/doc/tutorials/neb/idpp1.py
--- 3.24.0-1/doc/tutorials/neb/idpp1.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/neb/idpp1.py	2025-08-12 11:26:23.000000000 +0000
@@ -29,6 +29,7 @@ neb = NEB(images)
 neb.interpolate()
 
 # Run NEB calculation.
-qn = QuasiNewton(neb, trajectory='ethane_linear.traj',
-                 logfile='ethane_linear.log')
+qn = QuasiNewton(
+    neb, trajectory='ethane_linear.traj', logfile='ethane_linear.log'
+)
 qn.run(fmax=0.05)
diff -pruN 3.24.0-1/doc/tutorials/neb/idpp3.py 3.26.0-1/doc/tutorials/neb/idpp3.py
--- 3.24.0-1/doc/tutorials/neb/idpp3.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/neb/idpp3.py	2025-08-12 11:26:23.000000000 +0000
@@ -11,21 +11,20 @@ from ase.optimize.fire import FIRE as Qu
 nimages = 5
 
 # Some algebra to determine surface normal and the plane of the surface.
-d3 = [2., 1., 1.]
-a1 = np.array([0., 1., 1.])
+d3 = [2.0, 1.0, 1.0]
+a1 = np.array([0.0, 1.0, 1.0])
 d1 = np.cross(a1, d3)
-a2 = np.array([0., -1., 1.])
+a2 = np.array([0.0, -1.0, 1.0])
 d2 = np.cross(a2, d3)
 
 # Create the slab.
-slab = FaceCenteredCubic(directions=[d1, d2, d3],
-                         size=(2, 1, 2),
-                         symbol=('Pt'),
-                         latticeconstant=3.9)
+slab = FaceCenteredCubic(
+    directions=[d1, d2, d3], size=(2, 1, 2), symbol=('Pt'), latticeconstant=3.9
+)
 
 # Add some vacuum to the slab.
 uc = slab.get_cell()
-uc[2] += [0., 0., 10.]  # There are ten layers of vacuum.
+uc[2] += [0.0, 0.0, 10.0]  # There are ten layers of vacuum.
 uc = slab.set_cell(uc, scale_atoms=False)
 
 # Some positions needed to place the atom in the correct place.
@@ -50,7 +49,7 @@ relax = QuasiNewton(initial)
 relax.run(fmax=0.05)
 
 # Optimise the final state: atom above step.
-slab[-1].position = (x3, y2 + 1., z2 + 3.5)
+slab[-1].position = (x3, y2 + 1.0, z2 + 3.5)
 final = slab.copy()
 final.calc = EMT()
 relax = QuasiNewton(final)
diff -pruN 3.24.0-1/doc/tutorials/neb/idpp4.py 3.26.0-1/doc/tutorials/neb/idpp4.py
--- 3.24.0-1/doc/tutorials/neb/idpp4.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/neb/idpp4.py	2025-08-12 11:26:23.000000000 +0000
@@ -11,21 +11,20 @@ from ase.optimize.fire import FIRE as Qu
 nimages = 5
 
 # Some algebra to determine surface normal and the plane of the surface.
-d3 = [2., 1., 1.]
-a1 = np.array([0., 1., 1.])
+d3 = [2.0, 1.0, 1.0]
+a1 = np.array([0.0, 1.0, 1.0])
 d1 = np.cross(a1, d3)
-a2 = np.array([0., -1., 1.])
+a2 = np.array([0.0, -1.0, 1.0])
 d2 = np.cross(a2, d3)
 
 # Create the slab.
-slab = FaceCenteredCubic(directions=[d1, d2, d3],
-                         size=(2, 1, 2),
-                         symbol=('Pt'),
-                         latticeconstant=3.9)
+slab = FaceCenteredCubic(
+    directions=[d1, d2, d3], size=(2, 1, 2), symbol=('Pt'), latticeconstant=3.9
+)
 
 # Add some vacuum to the slab.
 uc = slab.get_cell()
-uc[2] += [0., 0., 10.]  # There are ten layers of vacuum.
+uc[2] += [0.0, 0.0, 10.0]  # There are ten layers of vacuum.
 uc = slab.set_cell(uc, scale_atoms=False)
 
 # Some positions needed to place the atom in the correct place.
@@ -50,7 +49,7 @@ relax = QuasiNewton(initial)
 relax.run(fmax=0.05)
 
 # Optimise the final state: atom above step.
-slab[-1].position = (x3, y2 + 1., z2 + 3.5)
+slab[-1].position = (x3, y2 + 1.0, z2 + 3.5)
 final = slab.copy()
 final.calc = EMT()
 relax = QuasiNewton(final)
@@ -71,6 +70,7 @@ neb = NEB(images)
 neb.interpolate()
 
 # Run NEB calculation.
-qn = QuasiNewton(neb, trajectory='N_diffusion_lin.traj',
-                 logfile='N_diffusion_lin.log')
+qn = QuasiNewton(
+    neb, trajectory='N_diffusion_lin.traj', logfile='N_diffusion_lin.log'
+)
 qn.run(fmax=0.05)
diff -pruN 3.24.0-1/doc/tutorials/povray_isosurface_tutorial.py 3.26.0-1/doc/tutorials/povray_isosurface_tutorial.py
--- 3.24.0-1/doc/tutorials/povray_isosurface_tutorial.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/povray_isosurface_tutorial.py	2025-08-12 11:26:23.000000000 +0000
@@ -19,7 +19,8 @@ povray_settings = {
     'canvas_height': 1024,  # Height of canvas in pixels
     'camera_dist': 25.0,  # Distance from camera to front atom
     'camera_type': 'orthographic angle 35',  # 'perspective angle 20'
-    'textures': len(atoms) * ['ase3']}
+    'textures': len(atoms) * ['ase3'],
+}
 
 # some more options:
 # 'image_plane'  : None,  # Distance from front atom to image plane
@@ -37,32 +38,40 @@ povray_settings = {
 generic_projection_settings = {
     'rotation': rotation,
     'radii': atoms.positions.shape[0] * [0.3],
-    'show_unit_cell': 1}
+    'show_unit_cell': 1,
+}
 
 # write returns a renderer object which needs to have the render method called
 
-write('NiO_marching_cubes1.pov', atoms,
-      **generic_projection_settings,
-      povray_settings=povray_settings,
-      isosurface_data=dict(density_grid=vchg.chgdiff[0],
-                           cut_off=density_cut_off)).render()
+write(
+    'NiO_marching_cubes1.pov',
+    atoms,
+    **generic_projection_settings,
+    povray_settings=povray_settings,
+    isosurface_data=dict(density_grid=vchg.chgdiff[0], cut_off=density_cut_off),
+).render()
 
 # spin up density, how to specify color and transparency r,g,b,t and a
 # material style from the standard ASE set
 
 
-write('NiO_marching_cubes2.pov', atoms,
-      **generic_projection_settings,
-      povray_settings=povray_settings,
-      isosurface_data=dict(density_grid=vchg.chgdiff[0],
-                           cut_off=density_cut_off,
-                           closed_edges=True,
-                           color=[0.25, 0.25, 0.80, 0.1],
-                           material='simple')).render()
+write(
+    'NiO_marching_cubes2.pov',
+    atoms,
+    **generic_projection_settings,
+    povray_settings=povray_settings,
+    isosurface_data=dict(
+        density_grid=vchg.chgdiff[0],
+        cut_off=density_cut_off,
+        closed_edges=True,
+        color=[0.25, 0.25, 0.80, 0.1],
+        material='simple',
+    ),
+).render()
 
 # spin down density, how to specify a povray material
 # that looks like pink jelly
-fun_material = '''
+fun_material = """
   material {
     texture {
       pigment { rgbt < 0.8, 0.25, 0.25, 0.5> }
@@ -79,12 +88,17 @@ roughness 0.001
       refraction on
       reflection on
       collect on
-  }'''
+  }"""
 
-write('NiO_marching_cubes3.pov', atoms,
-      **generic_projection_settings,
-      povray_settings=povray_settings,
-      isosurface_data=dict(density_grid=vchg.chgdiff[0],
-                           cut_off=-spin_cut_off,
-                           gradient_ascending=True,
-                           material=fun_material)).render()
+write(
+    'NiO_marching_cubes3.pov',
+    atoms,
+    **generic_projection_settings,
+    povray_settings=povray_settings,
+    isosurface_data=dict(
+        density_grid=vchg.chgdiff[0],
+        cut_off=-spin_cut_off,
+        gradient_ascending=True,
+        material=fun_material,
+    ),
+).render()
diff -pruN 3.24.0-1/doc/tutorials/qmmm/qmmm.rst 3.26.0-1/doc/tutorials/qmmm/qmmm.rst
--- 3.24.0-1/doc/tutorials/qmmm/qmmm.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/qmmm/qmmm.rst	2025-08-12 11:26:23.000000000 +0000
@@ -38,9 +38,9 @@ have a look at the :class:`~ase.calculat
 classes in any of the calculators above.
 
 .. _GPAW: https://gpaw.readthedocs.io/
-.. _DFTBplus: https://wiki.fysik.dtu.dk/ase/ase/calculators/dftb.html
-.. _CRYSTAL: https://wiki.fysik.dtu.dk/ase/ase/calculators/crystal.html
-.. _TURBOMOLE: https://wiki.fysik.dtu.dk/ase/ase/calculators/turbomole.html
+.. _DFTBplus: https://ase-lib.org/ase/calculators/dftb.html
+.. _CRYSTAL: https://ase-lib.org/ase/calculators/crystal.html
+.. _TURBOMOLE: https://ase-lib.org/ase/calculators/turbomole.html
 
 You might also be interested in the solvent MM potentials included in ASE.
 The tutorial on :ref:`tipnp water box equilibration` could be relevant to
diff -pruN 3.24.0-1/doc/tutorials/qmmm/water_dimer.py 3.26.0-1/doc/tutorials/qmmm/water_dimer.py
--- 3.24.0-1/doc/tutorials/qmmm/water_dimer.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/qmmm/water_dimer.py	2025-08-12 11:26:23.000000000 +0000
@@ -16,12 +16,14 @@ interaction = LJInteractions({('O', 'O')
 embedding = Embedding(rc=0.02)  # Short range analytical potential cutoff
 
 # Set up calculator
-atoms.calc = EIQMMM(qm_idx,
-                    GPAW(txt='qm.out'),
-                    TIP3P(),
-                    interaction,
-                    embedding=embedding,
-                    vacuum=None,  # if None, QM cell = MM cell
-                    output='qmmm.log')
+atoms.calc = EIQMMM(
+    qm_idx,
+    GPAW(txt='qm.out'),
+    TIP3P(),
+    interaction,
+    embedding=embedding,
+    vacuum=None,  # if None, QM cell = MM cell
+    output='qmmm.log',
+)
 
 print(atoms.get_potential_energy())
diff -pruN 3.24.0-1/doc/tutorials/saving_graphics.py 3.26.0-1/doc/tutorials/saving_graphics.py
--- 3.24.0-1/doc/tutorials/saving_graphics.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/saving_graphics.py	2025-08-12 11:26:23.000000000 +0000
@@ -16,7 +16,7 @@ rotation = '-70x, -20y, -2z'  # found us
 colors = hsv(atoms.positions[:, 0])
 
 # Textures
-tex = ['jmol', ] * 288 + ['glass', ] * 288 + ['ase3', ] * 288 + ['vmd', ] * 288
+tex = ['jmol'] * 288 + ['glass'] * 288 + ['ase3'] * 288 + ['vmd'] * 288
 
 
 # Keywords that exist for eps, png, and povs
@@ -32,15 +32,20 @@ povray_settings = {  # For povray files
     'transparent': False,  # Transparent background
     'canvas_width': None,  # Width of canvas in pixels
     'canvas_height': None,  # Height of canvas in pixels
-    'camera_dist': 50.,   # Distance from camera to front atom
+    'camera_dist': 50.0,  # Distance from camera to front atom
     'image_plane': None,  # Distance from front atom to image plane
     # (focal depth for perspective)
     'camera_type': 'perspective',  # perspective, ultra_wide_angle
-    'point_lights': [],             # [[loc1, color1], [loc2, color2],...]
-    'area_light': [(2., 3., 40.),  # location
-                   'White',       # color
-                   .7, .7, 3, 3],  # width, height, Nlamps_x, Nlamps_y
-    'background': 'White',        # color
+    'point_lights': [],  # [[loc1, color1], [loc2, color2],...]
+    'area_light': [
+        (2.0, 3.0, 40.0),  # location
+        'White',  # color
+        0.7,
+        0.7,
+        3,
+        3,
+    ],  # width, height, Nlamps_x, Nlamps_y
+    'background': 'White',  # color
     'textures': tex,  # Length of atoms list of texture names
     'celllinewidth': 0.05,  # Radius of the cylinders representing the cell
 }
@@ -51,12 +56,15 @@ povray_settings = {  # For povray files
 # Make the color of the glass beads semi-transparent
 colors2 = np.zeros((1152, 4))
 colors2[:, :3] = colors
-colors2[288: 576, 3] = 0.95
+colors2[288:576, 3] = 0.95
 generic_projection_settings['colors'] = colors2
 
 # Make the raytraced image
 # first write the configuration files, then call the external povray executable
-renderer = write('nice.pov', atoms,
-                 **generic_projection_settings,
-                 povray_settings=povray_settings)
+renderer = write(
+    'nice.pov',
+    atoms,
+    **generic_projection_settings,
+    povray_settings=povray_settings,
+)
 renderer.render()
diff -pruN 3.24.0-1/doc/tutorials/selfdiffusion/dimer_along.py 3.26.0-1/doc/tutorials/selfdiffusion/dimer_along.py
--- 3.24.0-1/doc/tutorials/selfdiffusion/dimer_along.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/selfdiffusion/dimer_along.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,5 @@
 """Dimer: Diffusion along rows"""
+
 from math import sqrt
 
 import numpy as np
@@ -14,11 +15,12 @@ from ase.optimize import QuasiNewton
 a = 4.0614
 b = a / sqrt(2)
 h = b / 2
-initial = Atoms('Al2',
-                positions=[(0, 0, 0),
-                           (a / 2, b / 2, -h)],
-                cell=(a, b, 2 * h),
-                pbc=(1, 1, 0))
+initial = Atoms(
+    'Al2',
+    positions=[(0, 0, 0), (a / 2, b / 2, -h)],
+    cell=(a, b, 2 * h),
+    pbc=(1, 1, 0),
+)
 initial *= (2, 2, 2)
 initial.append(Atom('Al', (a / 2, b / 2, 3 * h)))
 initial.center(vacuum=4.0, axis=2)
@@ -45,10 +47,12 @@ traj.write()
 d_mask = [False] * (N - 1) + [True]
 
 # Set up the dimer:
-d_control = DimerControl(initial_eigenmode_method='displacement',
-                         displacement_method='vector',
-                         logfile=None,
-                         mask=d_mask)
+d_control = DimerControl(
+    initial_eigenmode_method='displacement',
+    displacement_method='vector',
+    logfile=None,
+    mask=d_mask,
+)
 d_atoms = MinModeAtoms(initial, d_control)
 
 # Displacement settings:
@@ -60,9 +64,7 @@ displacement_vector[-1, 1] = 0.001
 d_atoms.displace(displacement_vector=displacement_vector)
 
 # Converge to a saddle point:
-dim_rlx = MinModeTranslate(d_atoms,
-                           trajectory=traj,
-                           logfile=None)
+dim_rlx = MinModeTranslate(d_atoms, trajectory=traj, logfile=None)
 dim_rlx.run(fmax=0.001)
 
 diff = initial.get_potential_energy() - e0
diff -pruN 3.24.0-1/doc/tutorials/selfdiffusion/neb1.py 3.26.0-1/doc/tutorials/selfdiffusion/neb1.py
--- 3.24.0-1/doc/tutorials/selfdiffusion/neb1.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/selfdiffusion/neb1.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,5 @@
 """Diffusion along rows"""
+
 from math import sqrt
 
 from ase import Atom, Atoms
@@ -12,11 +13,12 @@ from ase.visualize import view
 a = 4.0614
 b = a / sqrt(2)
 h = b / 2
-initial = Atoms('Al2',
-                positions=[(0, 0, 0),
-                           (a / 2, b / 2, -h)],
-                cell=(a, b, 2 * h),
-                pbc=(1, 1, 0))
+initial = Atoms(
+    'Al2',
+    positions=[(0, 0, 0), (a / 2, b / 2, -h)],
+    cell=(a, b, 2 * h),
+    pbc=(1, 1, 0),
+)
 initial *= (2, 2, 2)
 initial.append(Atom('Al', (a / 2, b / 2, 3 * h)))
 initial.center(vacuum=4.0, axis=2)
diff -pruN 3.24.0-1/doc/tutorials/selfdiffusion/neb2.py 3.26.0-1/doc/tutorials/selfdiffusion/neb2.py
--- 3.24.0-1/doc/tutorials/selfdiffusion/neb2.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/selfdiffusion/neb2.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,5 @@
 """Diffusion across rows"""
+
 from math import sqrt
 
 from ase import Atom, Atoms
@@ -12,11 +13,12 @@ from ase.visualize import view
 a = 4.0614
 b = a / sqrt(2)
 h = b / 2
-initial = Atoms('Al2',
-                positions=[(0, 0, 0),
-                           (a / 2, b / 2, -h)],
-                cell=(a, b, 2 * h),
-                pbc=(1, 1, 0))
+initial = Atoms(
+    'Al2',
+    positions=[(0, 0, 0), (a / 2, b / 2, -h)],
+    cell=(a, b, 2 * h),
+    pbc=(1, 1, 0),
+)
 initial *= (2, 2, 2)
 initial.append(Atom('Al', (a / 2, b / 2, 3 * h)))
 initial.center(vacuum=4.0, axis=2)
diff -pruN 3.24.0-1/doc/tutorials/selfdiffusion/neb3.py 3.26.0-1/doc/tutorials/selfdiffusion/neb3.py
--- 3.24.0-1/doc/tutorials/selfdiffusion/neb3.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/selfdiffusion/neb3.py	2025-08-12 11:26:23.000000000 +0000
@@ -1,4 +1,5 @@
 """Diffusion by an exchange process"""
+
 from math import sqrt
 
 from ase import Atom, Atoms
@@ -12,11 +13,12 @@ from ase.visualize import view
 a = 4.0614
 b = a / sqrt(2)
 h = b / 2
-initial = Atoms('Al2',
-                positions=[(0, 0, 0),
-                           (a / 2, b / 2, -h)],
-                cell=(a, b, 2 * h),
-                pbc=(1, 1, 0))
+initial = Atoms(
+    'Al2',
+    positions=[(0, 0, 0), (a / 2, b / 2, -h)],
+    cell=(a, b, 2 * h),
+    pbc=(1, 1, 0),
+)
 initial *= (2, 2, 2)
 initial.append(Atom('Al', (a / 2, b / 2, 3 * h)))
 initial.center(vacuum=4.0, axis=2)
diff -pruN 3.24.0-1/doc/tutorials/selfdiffusion/plot.py 3.26.0-1/doc/tutorials/selfdiffusion/plot.py
--- 3.24.0-1/doc/tutorials/selfdiffusion/plot.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/selfdiffusion/plot.py	2025-08-12 11:26:23.000000000 +0000
@@ -9,15 +9,16 @@ from ase.io import write
 a = 4.0614
 b = a / sqrt(2)
 h = b / 2
-atoms = Atoms('Al2',
-              positions=[(0, 0, 0),
-                         (a / 2, b / 2, -h)],
-              cell=(a, b, 2 * h),
-              pbc=(1, 1, 0))
+atoms = Atoms(
+    'Al2',
+    positions=[(0, 0, 0), (a / 2, b / 2, -h)],
+    cell=(a, b, 2 * h),
+    pbc=(1, 1, 0),
+)
 
 atoms *= (2, 2, 2)
 atoms.append(Atom('Al', (a / 2, b / 2, 3 * h)))
-atoms.center(vacuum=4., axis=2)
+atoms.center(vacuum=4.0, axis=2)
 
 atoms *= (2, 3, 1)
 atoms.cell /= [2, 3, 1]
@@ -27,11 +28,12 @@ radii = 1.2
 colors = jmol_colors[atoms.numbers]
 colors[16::17] = [1, 0, 0]
 
-renderer = write('Al110slab.pov', atoms,
-                 rotation=rotation,
-                 colors=colors,
-                 radii=radii,
-                 povray_settings=dict(
-                     canvas_width=500,
-                     transparent=False))
+renderer = write(
+    'Al110slab.pov',
+    atoms,
+    rotation=rotation,
+    colors=colors,
+    radii=radii,
+    povray_settings=dict(canvas_width=500, transparent=False),
+)
 renderer.render()
diff -pruN 3.24.0-1/doc/tutorials/tipnp_equil/tip3p_equil.py 3.26.0-1/doc/tutorials/tipnp_equil/tip3p_equil.py
--- 3.24.0-1/doc/tutorials/tipnp_equil/tip3p_equil.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/tipnp_equil/tip3p_equil.py	2025-08-12 11:26:23.000000000 +0000
@@ -9,12 +9,14 @@ from ase.md import Langevin
 
 # Set up water box at 20 deg C density
 x = angleHOH * np.pi / 180 / 2
-pos = [[0, 0, 0],
-       [0, rOH * np.cos(x), rOH * np.sin(x)],
-       [0, rOH * np.cos(x), -rOH * np.sin(x)]]
+pos = [
+    [0, 0, 0],
+    [0, rOH * np.cos(x), rOH * np.sin(x)],
+    [0, rOH * np.cos(x), -rOH * np.sin(x)],
+]
 atoms = Atoms('OH2', positions=pos)
 
-vol = ((18.01528 / 6.022140857e23) / (0.9982 / 1e24))**(1 / 3.)
+vol = ((18.01528 / 6.022140857e23) / (0.9982 / 1e24)) ** (1 / 3.0)
 atoms.set_cell((vol, vol, vol))
 atoms.center()
 
@@ -22,14 +24,19 @@ atoms = atoms.repeat((3, 3, 3))
 atoms.set_pbc(True)
 
 # RATTLE-type constraints on O-H1, O-H2, H1-H2.
-atoms.constraints = FixBondLengths([(3 * i + j, 3 * i + (j + 1) % 3)
-                                    for i in range(3**3)
-                                    for j in [0, 1, 2]])
+atoms.constraints = FixBondLengths(
+    [(3 * i + j, 3 * i + (j + 1) % 3) for i in range(3**3) for j in [0, 1, 2]]
+)
 
 tag = 'tip3p_27mol_equil'
 atoms.calc = TIP3P(rc=4.5)
-md = Langevin(atoms, 1 * units.fs, temperature=300 * units.kB,
-              friction=0.01, logfile=tag + '.log')
+md = Langevin(
+    atoms,
+    1 * units.fs,
+    temperature=300 * units.kB,
+    friction=0.01,
+    logfile=tag + '.log',
+)
 
 traj = Trajectory(tag + '.traj', 'w', atoms)
 md.attach(traj.write, interval=1)
@@ -39,12 +46,21 @@ md.run(4000)
 tag = 'tip3p_216mol_equil'
 atoms.set_constraint()  # repeat not compatible with FixBondLengths currently.
 atoms = atoms.repeat((2, 2, 2))
-atoms.constraints = FixBondLengths([(3 * i + j, 3 * i + (j + 1) % 3)
-                                    for i in range(len(atoms) / 3)
-                                    for j in [0, 1, 2]])
-atoms.calc = TIP3P(rc=7.)
-md = Langevin(atoms, 2 * units.fs, temperature=300 * units.kB,
-              friction=0.01, logfile=tag + '.log')
+atoms.constraints = FixBondLengths(
+    [
+        (3 * i + j, 3 * i + (j + 1) % 3)
+        for i in range(len(atoms) / 3)
+        for j in [0, 1, 2]
+    ]
+)
+atoms.calc = TIP3P(rc=7.0)
+md = Langevin(
+    atoms,
+    2 * units.fs,
+    temperature=300 * units.kB,
+    friction=0.01,
+    logfile=tag + '.log',
+)
 
 traj = Trajectory(tag + '.traj', 'w', atoms)
 md.attach(traj.write, interval=1)
diff -pruN 3.24.0-1/doc/tutorials/tut06_database/database.rst 3.26.0-1/doc/tutorials/tut06_database/database.rst
--- 3.24.0-1/doc/tutorials/tut06_database/database.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/tut06_database/database.rst	2025-08-12 11:26:23.000000000 +0000
@@ -180,7 +180,7 @@ in action you can check out the 2D datab
 Adsorbates on metals
 --------------------
 When you are done with this introductory exercise we encourage you to follow
-the online ASE-DB tutorial at https://wiki.fysik.dtu.dk/ase/tutorials/db/db.html.
+the online ASE-DB tutorial at https://ase-lib.org/tutorials/db/db.html.
 
 
 Solutions
diff -pruN 3.24.0-1/doc/tutorials/tut06_database/solution/solution.py 3.26.0-1/doc/tutorials/tut06_database/solution/solution.py
--- 3.24.0-1/doc/tutorials/tut06_database/solution/solution.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/tut06_database/solution/solution.py	2025-08-12 11:26:23.000000000 +0000
@@ -19,9 +19,9 @@ for f in structures:
 
 for row in db.select():
     atoms = row.toatoms()
-    calc = GPAW(mode=PW(400),
-                kpts=(4, 4, 4),
-                txt=f'{row.formula}-gpaw.txt', xc='LDA')
+    calc = GPAW(
+        mode=PW(400), kpts=(4, 4, 4), txt=f'{row.formula}-gpaw.txt', xc='LDA'
+    )
     atoms.calc = calc
     atoms.get_stress()
     filter = ExpCellFilter(atoms)
@@ -32,9 +32,9 @@ for row in db.select():
 
 for row in db.select(relaxed=True):
     atoms = row.toatoms()
-    calc = GPAW(mode=PW(400),
-                kpts=(4, 4, 4),
-                txt=f'{row.formula}-gpaw.txt', xc='LDA')
+    calc = GPAW(
+        mode=PW(400), kpts=(4, 4, 4), txt=f'{row.formula}-gpaw.txt', xc='LDA'
+    )
     atoms.calc = calc
     atoms.get_potential_energy()
     bg, _, _ = bandgap(calc=atoms.calc)
diff -pruN 3.24.0-1/doc/tutorials/tutorials.rst 3.26.0-1/doc/tutorials/tutorials.rst
--- 3.24.0-1/doc/tutorials/tutorials.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/tutorials.rst	2025-08-12 11:26:23.000000000 +0000
@@ -101,7 +101,7 @@ For more details:
 
 * Look at the documentation for the individual :ref:`modules <ase>`.
 * Browse the :git:`source code <>` online.
-
+* `External tutorial part of Openscience and ASE workshop, Daresbury 2023  <https://ase-workshop-2023.github.io/tutorial/>`_
 
 Videos
 ------
diff -pruN 3.24.0-1/doc/tutorials/wannier/benzene.py 3.26.0-1/doc/tutorials/wannier/benzene.py
--- 3.24.0-1/doc/tutorials/wannier/benzene.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/wannier/benzene.py	2025-08-12 11:26:23.000000000 +0000
@@ -5,13 +5,16 @@ from ase.build import molecule
 atoms = molecule('C6H6')
 atoms.center(vacuum=3.5)
 
-calc = GPAW(h=.21, xc='PBE', txt='benzene.txt', nbands=18)
+calc = GPAW(mode='fd', h=0.21, xc='PBE', txt='benzene.txt', nbands=18)
 atoms.calc = calc
 atoms.get_potential_energy()
 
-calc = calc.fixed_density(txt='benzene-harris.txt',
-                          nbands=40, eigensolver='cg',
-                          convergence={'bands': 35})
+calc = calc.fixed_density(
+    txt='benzene-harris.txt',
+    nbands=40,
+    eigensolver='cg',
+    convergence={'bands': 35},
+)
 atoms.get_potential_energy()
 
 calc.write('benzene.gpw', mode='all')
diff -pruN 3.24.0-1/doc/tutorials/wannier/plot_band_structure.py 3.26.0-1/doc/tutorials/wannier/plot_band_structure.py
--- 3.24.0-1/doc/tutorials/wannier/plot_band_structure.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/wannier/plot_band_structure.py	2025-08-12 11:26:23.000000000 +0000
@@ -2,7 +2,7 @@ import matplotlib.pyplot as plt
 import numpy as np
 
 fig = plt.figure(dpi=80, figsize=(4.2, 6))
-fig.subplots_adjust(left=.16, right=.97, top=.97, bottom=.05)
+fig.subplots_adjust(left=0.16, right=0.97, top=0.97, bottom=0.05)
 
 # Plot KS bands
 k, eps = np.loadtxt('KSbands.txt', unpack=True)
@@ -12,11 +12,14 @@ plt.plot(k, eps, 'ro', label='DFT', ms=9
 k, eps = np.loadtxt('WANbands.txt', unpack=True)
 plt.plot(k, eps, 'k.', label='Wannier')
 
-plt.plot([-.5, .5], [1, 1], 'k:', label='_nolegend_')
-plt.text(-.5, 1, 'fixedenergy', ha='left', va='bottom')
+plt.plot([-0.5, 0.5], [1, 1], 'k:', label='_nolegend_')
+plt.text(-0.5, 1, 'fixedenergy', ha='left', va='bottom')
 plt.axis('tight')
-plt.xticks([-.5, -.25, 0, .25, .5],
-           [r'$X$', r'$\Delta$', r'$\Gamma$', r'$\Delta$', r'$X$'], size=16)
+plt.xticks(
+    [-0.5, -0.25, 0, 0.25, 0.5],
+    [r'$X$', r'$\Delta$', r'$\Gamma$', r'$\Delta$', r'$X$'],
+    size=16,
+)
 plt.ylabel(r'$E - E_F\  \rm{(eV)}$', size=16)
 plt.legend()
 plt.savefig('bands.png', dpi=80)
diff -pruN 3.24.0-1/doc/tutorials/wannier/plot_spectral_weight.py 3.26.0-1/doc/tutorials/wannier/plot_spectral_weight.py
--- 3.24.0-1/doc/tutorials/wannier/plot_spectral_weight.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/wannier/plot_spectral_weight.py	2025-08-12 11:26:23.000000000 +0000
@@ -7,13 +7,21 @@ from ase.dft.wannier import Wannier
 atoms, calc = restart('benzene.gpw', txt=None)
 wan = Wannier(nwannier=18, calc=calc, fixedstates=15, file='wan18.json')
 
-weight_n = np.sum(abs(wan.V_knw[0])**2, 1)
+weight_n = np.sum(abs(wan.V_knw[0]) ** 2, 1)
 N = len(weight_n)
 F = wan.fixedstates_k[0]
 plt.figure(1, figsize=(12, 4))
-plt.bar(range(1, N + 1), weight_n, width=0.65, bottom=0,
-        color='k', edgecolor='k', linewidth=None,
-        align='center', orientation='vertical')
+plt.bar(
+    range(1, N + 1),
+    weight_n,
+    width=0.65,
+    bottom=0,
+    color='k',
+    edgecolor='k',
+    linewidth=None,
+    align='center',
+    orientation='vertical',
+)
 plt.plot([F + 0.5, F + 0.5], [0, 1], 'k--')
 plt.axis(xmin=0.32, xmax=N + 1.33, ymin=0, ymax=1)
 plt.xlabel('Eigenstate')
diff -pruN 3.24.0-1/doc/tutorials/wannier/polyacetylene.py 3.26.0-1/doc/tutorials/wannier/polyacetylene.py
--- 3.24.0-1/doc/tutorials/wannier/polyacetylene.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/wannier/polyacetylene.py	2025-08-12 11:26:23.000000000 +0000
@@ -4,20 +4,31 @@ from gpaw import GPAW
 from ase import Atoms
 from ase.dft.kpoints import monkhorst_pack
 
-kpts = monkhorst_pack((13, 1, 1)) + [1e-5, 0, 0]
-calc = GPAW(h=.21, xc='PBE', kpts=kpts, nbands=12, txt='poly.txt',
-            eigensolver='cg', convergence={'bands': 9})
+kpts = monkhorst_pack((13, 1, 1))
+calc = GPAW(
+    mode='fd',
+    h=0.21,
+    xc='PBE',
+    kpts=kpts,
+    nbands=12,
+    txt='poly.txt',
+    eigensolver='cg',
+    convergence={'bands': 9},
+    symmetry='off',
+)
 
 CC = 1.38
 CH = 1.094
 a = 2.45
-x = a / 2.
+x = a / 2.0
 y = np.sqrt(CC**2 - x**2)
-atoms = Atoms('C2H2', pbc=(True, False, False), cell=(a, 8., 6.),
-              calculator=calc, positions=[[0, 0, 0],
-                                          [x, y, 0],
-                                          [x, y + CH, 0],
-                                          [0, -CH, 0]])
+atoms = Atoms(
+    'C2H2',
+    pbc=(True, False, False),
+    cell=(a, 8.0, 6.0),
+    calculator=calc,
+    positions=[[0, 0, 0], [x, y, 0], [x, y + CH, 0], [0, -CH, 0]],
+)
 atoms.center()
 atoms.get_potential_energy()
 calc.write('poly.gpw', mode='all')
diff -pruN 3.24.0-1/doc/tutorials/wannier/wannier.rst 3.26.0-1/doc/tutorials/wannier/wannier.rst
--- 3.24.0-1/doc/tutorials/wannier/wannier.rst	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/wannier/wannier.rst	2025-08-12 11:26:23.000000000 +0000
@@ -1,12 +1,97 @@
 .. _wannier tutorial:
-    
-=================================
+
+============================================
 Partly occupied Wannier Functions
-=================================
+============================================
+
+This tutorial walks through building **partly occupied Wannier
+functions** with the :mod:`ase.dft.wannier` module
+and the `GPAW <https://wiki.fysik.dtu.dk/gpaw/>`_ electronic structure code. 
+For more information on the details of the method and the implementation, see
+
+   | K. S. Thygesen, L. B. Hansen, and K. W. Jacobsen
+   | :doi:`Partly occupied Wannier functions: Construction and applications <https://doi.org/10.1103/PhysRevB.72.125119 >`
+   | Phys. Rev. B 72, 125119, (2005)
+
+.. contents:: **Outline**
+   :depth: 2
+   :local:
+
+
+Benzene molecule
+================
+
+Step 1 – Ground-state calculation
+---------------------------------
+
+Run the script below to obtain the ground-state density and the
+Kohn–Sham (KS) orbitals. The result is stored in :file:`benzene.gpw`.
 
 .. literalinclude:: benzene.py
+   :language: python
+
+Step 2 – Maximally localized WFs for the occupied subspace (15 WFs)
+-------------------------------------------------------------------
+
+There are 15 occupied bands in the benzene molecule. We construct one Wannier function per occupied band by setting
+``nwannier = 15``. 
+By calling ``wan.localize()``, the code attempts to minimize the spread functional using a gradient-descent algorithm. 
+The resulting WFs are written to .cube files, which allows them to be inspected using e.g. VESTA.
+
 .. literalinclude:: wannier_benzene.py
+   :language: python
+
+Step 3 – Adding three extra degrees of freedom (18 WFs)
+-------------------------------------------------------
+
+To improve localization we augment the basis with three extra Wannier functions - so-called *extra degrees of freedom*
+(``nwannier = 18``, ``fixedstates = 15``). This will allow the Wannierization procedure to use the unoccupied states to minimize spread functional.
+
+.. literalinclude:: wannier_benzene_with_edf.py
+   :language: python
+
+
+Step 4 – Spectral-weight analysis
+---------------------------------
+
+The script below projects the WFs on the KS eigenstates. You should see
+the 15 lowest bands perfectly reconstructed (weight ≃ 1.0) while higher
+bands are only partially represented.
+
 .. literalinclude:: plot_spectral_weight.py
+   :language: python
+
+
+Polyacetylene chain (1-D periodic)
+==================================
+
+We now want to construct partially occupied Wannier functions to describe a polyacetylene chain.
+
+Step 1 – Structure & ground-state calculation
+---------------------------------------------
+
+Polyacetylene is modelled as an infinite chain; we therefore enable
+periodic boundary conditions along *x*.
+
 .. literalinclude:: polyacetylene.py
+   :language: python
+
+Step 2 – Wannierization
+-----------------------
+
+We repeat the localization procedure, keeping the five lowest
+bands fixed and adding one extra degree of freedom to aid localization.
+
 .. literalinclude:: wannier_polyacetylene.py
+   :language: python
+
+Step 3 – High-resolution band structure
+---------------------------------------
+
+Using the Wannier Hamiltonian we can interpolate the band structure on a
+fine 100-point *k* mesh and compare it to the original DFT result. 
+
 .. literalinclude:: plot_band_structure.py
+   :language: python
+
+Within the fixed-energy window—that is, for energies below the fixed-energy line—the Wannier-interpolated bands coincide perfectly with the DFT reference (red circles). Above this window the match is lost, because the degrees of freedom deliberately mix several Kohn–Sham states to achieve maximal real-space localisation.
\ No newline at end of file
diff -pruN 3.24.0-1/doc/tutorials/wannier/wannier_benzene.py 3.26.0-1/doc/tutorials/wannier/wannier_benzene.py
--- 3.24.0-1/doc/tutorials/wannier/wannier_benzene.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/wannier/wannier_benzene.py	2025-08-12 11:26:23.000000000 +0000
@@ -9,10 +9,3 @@ wan = Wannier(nwannier=15, calc=calc)
 wan.localize()
 for i in range(wan.nwannier):
     wan.write_cube(i, 'benzene15_%i.cube' % i)
-
-# Make wannier functions using (three) extra degrees of freedom.
-wan = Wannier(nwannier=18, calc=calc, fixedstates=15)
-wan.localize()
-wan.save('wan18.json')
-for i in range(wan.nwannier):
-    wan.write_cube(i, 'benzene18_%i.cube' % i)
diff -pruN 3.24.0-1/doc/tutorials/wannier/wannier_benzene_with_edf.py 3.26.0-1/doc/tutorials/wannier/wannier_benzene_with_edf.py
--- 3.24.0-1/doc/tutorials/wannier/wannier_benzene_with_edf.py	1970-01-01 00:00:00.000000000 +0000
+++ 3.26.0-1/doc/tutorials/wannier/wannier_benzene_with_edf.py	2025-08-12 11:26:23.000000000 +0000
@@ -0,0 +1,12 @@
+from gpaw import restart
+
+from ase.dft.wannier import Wannier
+
+atoms, calc = restart('benzene.gpw', txt=None)
+
+# Make wannier functions using (three) extra degrees of freedom.
+wan = Wannier(nwannier=18, calc=calc, fixedstates=15)
+wan.localize()
+wan.save('wan18.json')
+for i in range(wan.nwannier):
+    wan.write_cube(i, 'benzene18_%i.cube' % i)
diff -pruN 3.24.0-1/doc/tutorials/wannier/wannier_polyacetylene.py 3.26.0-1/doc/tutorials/wannier/wannier_polyacetylene.py
--- 3.24.0-1/doc/tutorials/wannier/wannier_polyacetylene.py	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/doc/tutorials/wannier/wannier_polyacetylene.py	2025-08-12 11:26:23.000000000 +0000
@@ -6,9 +6,13 @@ from ase.dft.wannier import Wannier
 atoms, calc = restart('poly.gpw', txt=None)
 
 # Make wannier functions using (one) extra degree of freedom
-wan = Wannier(nwannier=6, calc=calc, fixedenergy=1.5,
-              initialwannier='orbitals',
-              functional='var')
+wan = Wannier(
+    nwannier=6,
+    calc=calc,
+    fixedenergy=1.5,
+    initialwannier='orbitals',
+    functional='var',
+)
 wan.localize()
 wan.save('poly.json')
 wan.translate_all_to_cell((2, 0, 0))
@@ -24,7 +28,7 @@ with open('KSbands.txt', 'w') as fd:
 
 # Print Wannier bandstructure
 with open('WANbands.txt', 'w') as fd:
-    for k in np.linspace(-.5, .5, 100):
+    for k in np.linspace(-0.5, 0.5, 100):
         ham = wan.get_hamiltonian_kpoint([k, 0, 0])
         for eps in np.linalg.eigvalsh(ham).real:
             print(k, eps - ef, file=fd)
diff -pruN 3.24.0-1/pyproject.toml 3.26.0-1/pyproject.toml
--- 3.24.0-1/pyproject.toml	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/pyproject.toml	2025-08-12 11:26:23.000000000 +0000
@@ -7,11 +7,11 @@ name = 'ase'
 description='Atomic Simulation Environment'
 dynamic = ['version']
 readme = 'README.rst'
-license = { text = 'LGPLv2.1+' }
+license = 'LGPL-2.1-or-later'
+license-files = ['LICENSE']
 maintainers = [{ name = 'ASE Community', email = 'ase-users@listserv.fysik.dtu.dk' }]
 classifiers = [
     'Development Status :: 6 - Mature',
-    'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)',
     'Intended Audience :: Science/Research',
     'Operating System :: OS Independent',
     'Programming Language :: Python :: 3',
@@ -30,7 +30,7 @@ dependencies = [
 ]
 
 [project.urls]
-Homepage = 'https://wiki.fysik.dtu.dk/ase/'
+Homepage = 'https://ase-lib.org/'
 Repository = 'https://gitlab.com/ase/ase.git'
 Issues = 'https://gitlab.com/ase/ase/issues/'
 
@@ -38,9 +38,10 @@ Issues = 'https://gitlab.com/ase/ase/iss
 ase = 'ase.cli.main:main'
 
 [project.optional-dependencies]
-docs = ['sphinx', 'sphinx_rtd_theme', 'pillow']
+docs = ['sphinx', 'sphinx_book_theme', 'pillow']
 test = ['pytest>=7.0.0', 'pytest-xdist>=2.1.0']
 spglib = ['spglib>=1.9']
+lint = ['mypy', 'ruff', 'coverage', 'types-docutils', 'types-PyMySQL', 'flake8']
 
 [tool.setuptools.package-data]
 ase = [
@@ -59,10 +60,10 @@ version = {attr = 'ase.__version__'}
 [tool.pytest.ini_options]
 testpaths = ['ase/test']
 markers = [
-    "calculator: parametrizes calculator tests with calculator factories",
-    "calculator_lite: for calculator tests; include in calculators-lite job",
-    "optimize: tests of optimizers",
-    "slow: test takes longer than a few seconds",
+    'calculator: parametrizes calculator tests with calculator factories',
+    'calculator_lite: for calculator tests; include in calculators-lite job',
+    'optimize: tests of optimizers',
+    'slow: test takes longer than a few seconds',
 ]
 
 [tool.mypy]
@@ -81,24 +82,24 @@ exclude_also = [
 
 [tool.ruff]
 line-length = 80
-exclude = ["./build/", "./dist", "./doc/build"]
-src = ["ase"]
+exclude = ['./build/', './dist', './doc/build']
+src = ['ase', 'doc']
 
 [tool.ruff.format]
-quote-style = "single"
+quote-style = 'single'
 skip-magic-trailing-comma = false
 
 [tool.ruff.lint]
 preview = true  # necessary to activate many pycodestyle rules
 select = [
-    "F",  # Pyflakes
-    "E",  # pycodestyle
-    "W",  # pycodestyle
-    "I"   # isort
+    'F',  # Pyflakes
+    'E',  # pycodestyle
+    'W',  # pycodestyle
+    'I'   # isort
 ]
 ignore = [
-    "E741",  # ambiguous-variable-name
-    "W505",  # doc-line-too-long
+    'E741',  # ambiguous-variable-name
+    'W505',  # doc-line-too-long
 ]
 
 [tool.ruff.lint.pycodestyle]
@@ -106,8 +107,23 @@ max-line-length = 80
 max-doc-length = 80
 
 [tool.ruff.lint.pydocstyle]
-convention = "numpy"
+convention = 'numpy'
 
 [tool.ruff.lint.isort]
-known-first-party = ["ase"]
+known-first-party = ['ase']
 
+[tool.scriv]
+output_file = 'doc/changelog.rst'
+insert_marker = 'scriv-auto-changelog-start'
+end_marker = 'scriv-auto-changelog-end'
+categories = [
+    'I/O',
+    'Calculators',
+    'Optimizers',
+    'Molecular dynamics',
+    'GUI',
+    'Development',
+    'Documentation',
+    'Other changes',
+    'Bugfixes'
+]
diff -pruN 3.24.0-1/tools/ase-build 3.26.0-1/tools/ase-build
--- 3.24.0-1/tools/ase-build	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/tools/ase-build	1970-01-01 00:00:00.000000000 +0000
@@ -1,4 +0,0 @@
-#!/usr/bin/env python3
-from ase.cli.main import old
-
-old()
diff -pruN 3.24.0-1/tools/ase-db 3.26.0-1/tools/ase-db
--- 3.24.0-1/tools/ase-db	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/tools/ase-db	1970-01-01 00:00:00.000000000 +0000
@@ -1,4 +0,0 @@
-#!/usr/bin/env python3
-from ase.cli.main import old
-
-old()
diff -pruN 3.24.0-1/tools/ase-gui 3.26.0-1/tools/ase-gui
--- 3.24.0-1/tools/ase-gui	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/tools/ase-gui	1970-01-01 00:00:00.000000000 +0000
@@ -1,4 +0,0 @@
-#!/usr/bin/env python3
-from ase.cli.main import old
-
-old()
diff -pruN 3.24.0-1/tools/ase-info 3.26.0-1/tools/ase-info
--- 3.24.0-1/tools/ase-info	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/tools/ase-info	1970-01-01 00:00:00.000000000 +0000
@@ -1,4 +0,0 @@
-#!/usr/bin/env python3
-from ase.cli.main import old
-
-old()
diff -pruN 3.24.0-1/tools/ase-run 3.26.0-1/tools/ase-run
--- 3.24.0-1/tools/ase-run	2024-12-28 21:22:10.000000000 +0000
+++ 3.26.0-1/tools/ase-run	1970-01-01 00:00:00.000000000 +0000
@@ -1,4 +0,0 @@
-#!/usr/bin/env python3
-from ase.cli.main import old
-
-old()
