Repository: materialsproject/pymatgen

Issue:
typo in 'core/structure.py'
Hi, I noticed a small typo in the comment in <kbd>core/structure.py</kbd>. The comment mentions "translation," but the function actually implements a "rotation" operation. (See line 4677 & 5298)
https://github.com/materialsproject/pymatgen/blob/153ccb46155f665b9933babfd48b563ac24dfaca/src/pymatgen/core/structure.py#L5287-L5299
https://github.com/materialsproject/pymatgen/blob/153ccb46155f665b9933babfd48b563ac24dfaca/src/pymatgen/core/structure.py#L4664-L4678

Hints from issue comments:
@DanielYang59: Thanks for catching this, I would fix it!

Solution patch:
diff --git a/pyproject.toml b/pyproject.toml
index 4bef5c2d6b..bfc03d3f96 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -60,7 +60,7 @@ dependencies = [
     "networkx>=2.7",  # PR4116
     "palettable>=3.3.3",
     "pandas>=2",
-    "plotly>=4.5.0,<6.0.0",
+    "plotly>=5.0.0",
     "pybtex>=0.24.0",
     "requests>=2.32",
     "ruamel.yaml>=0.17.0",
diff --git a/src/pymatgen/analysis/eos.py b/src/pymatgen/analysis/eos.py
index f25844eaec..67f171eba4 100644
--- a/src/pymatgen/analysis/eos.py
+++ b/src/pymatgen/analysis/eos.py
@@ -24,7 +24,8 @@ from pymatgen.core.units import FloatWithUnit
 from pymatgen.util.plotting import add_fig_kwargs, get_ax_fig, pretty_plot
 
 if TYPE_CHECKING:
-    from typing import ClassVar
+    from collections.abc import Sequence
+    from typing import Any, ClassVar
 
     import matplotlib.pyplot as plt
 
@@ -40,7 +41,11 @@ class EOSBase(ABC):
     implementations.
     """
 
-    def __init__(self, volumes, energies):
+    def __init__(
+        self,
+        volumes: Sequence[float],
+        energies: Sequence[float],
+    ) -> None:
         """
         Args:
             volumes (Sequence[float]): in Ang^3.
@@ -50,18 +55,28 @@ class EOSBase(ABC):
         self.energies = np.array(energies)
         # minimum energy(e0), buk modulus(b0),
         # derivative of bulk modulus w.r.t. pressure(b1), minimum volume(v0)
-        self._params = None
+        self._params: Sequence | None = None
         # the eos function parameters. It is the same as _params except for
         # equation of states that uses polynomial fits(delta_factor and
         # numerical_eos)
-        self.eos_params = None
+        self.eos_params: Sequence | None = None
 
-    def _initial_guess(self):
+    def __call__(self, volume: float) -> float:
+        """
+        Args:
+            volume (float | list[float]): volume(s) in Ang^3.
+
+        Returns:
+            Compute EOS with this volume.
+        """
+        return self.func(volume)
+
+    def _initial_guess(self) -> tuple[float, float, float, float]:
         """
         Quadratic fit to get an initial guess for the parameters.
 
         Returns:
-            tuple: 4 floats for (e0, b0, b1, v0)
+            tuple[float, float, float, float]: e0, b0, b1, v0
         """
         a, b, c = np.polyfit(self.volumes, self.energies, 2)
         self.eos_params = [a, b, c]
@@ -78,7 +93,7 @@ class EOSBase(ABC):
 
         return e0, b0, b1, v0
 
-    def fit(self):
+    def fit(self) -> None:
         """
         Do the fitting. Does least square fitting. If you want to use custom
         fitting, must override this.
@@ -120,24 +135,20 @@ class EOSBase(ABC):
         """
         return self._func(np.array(volume), self.eos_params)
 
-    def __call__(self, volume: float) -> float:
-        """
-        Args:
-            volume (float | list[float]): volume(s) in Ang^3.
-
-        Returns:
-            Compute EOS with this volume.
-        """
-        return self.func(volume)
-
     @property
     def e0(self) -> float:
         """The min energy."""
+        if self._params is None:
+            raise RuntimeError("params have not be initialized.")
+
         return self._params[0]
 
     @property
     def b0(self) -> float:
         """The bulk modulus in units of energy/unit of volume^3."""
+        if self._params is None:
+            raise RuntimeError("params have not be initialized.")
+
         return self._params[1]
 
     @property
@@ -156,11 +167,18 @@ class EOSBase(ABC):
         return self._params[3]
 
     @property
-    def results(self):
+    def results(self) -> dict[str, Any]:
         """A summary dict."""
         return {"e0": self.e0, "b0": self.b0, "b1": self.b1, "v0": self.v0}
 
-    def plot(self, width=8, height=None, ax: plt.Axes = None, dpi=None, **kwargs):
+    def plot(
+        self,
+        width: float = 8,
+        height: float | None = None,
+        ax: plt.Axes = None,
+        dpi: float | None = None,
+        **kwargs,
+    ) -> plt.Axes:
         """
         Plot the equation of state.
 
@@ -170,7 +188,7 @@ class EOSBase(ABC):
                 golden ratio.
             ax (plt.Axes): If supplied, changes will be made to the existing Axes.
                 Otherwise, new Axes will be created.
-            dpi:
+            dpi (float): DPI.
             kwargs (dict): additional args fed to pyplot.plot.
                 supported keys: style, color, text, label
 
@@ -211,16 +229,18 @@ class EOSBase(ABC):
         return ax
 
     @add_fig_kwargs
-    def plot_ax(self, ax: plt.Axes = None, fontsize=12, **kwargs):
+    def plot_ax(
+        self,
+        ax: plt.Axes | None = None,
+        fontsize: float = 12,
+        **kwargs,
+    ) -> plt.Figure:
         """
         Plot the equation of state on axis `ax`.
 
         Args:
             ax: matplotlib Axes or None if a new figure should be created.
             fontsize: Legend fontsize.
-            color (str): plot color.
-            label (str): Plot label
-            text (str): Legend text (options)
 
         Returns:
             plt.Figure: matplotlib figure.
@@ -270,7 +290,7 @@ class EOSBase(ABC):
 class Murnaghan(EOSBase):
     """Murnaghan EOS."""
 
-    def _func(self, volume, params):
+    def _func(self, volume, params: tuple[float, float, float, float]):
         """From PRB 28,5480 (1983)."""
         e0, b0, b1, v0 = tuple(params)
         return e0 + b0 * volume / b1 * (((v0 / volume) ** b1) / (b1 - 1.0) + 1.0) - v0 * b0 / (b1 - 1.0)
@@ -279,7 +299,7 @@ class Murnaghan(EOSBase):
 class Birch(EOSBase):
     """Birch EOS."""
 
-    def _func(self, volume, params):
+    def _func(self, volume, params: tuple[float, float, float, float]):
         """From Intermetallic compounds: Principles and Practice, Vol. I:
         Principles Chapter 9 pages 195-210 by M. Mehl. B. Klein,
         D. Papaconstantopoulos.
@@ -296,7 +316,7 @@ class Birch(EOSBase):
 class BirchMurnaghan(EOSBase):
     """BirchMurnaghan EOS."""
 
-    def _func(self, volume, params):
+    def _func(self, volume, params: tuple[float, float, float, float]):
         """BirchMurnaghan equation from PRB 70, 224107."""
         e0, b0, b1, v0 = tuple(params)
         eta = (v0 / volume) ** (1 / 3)
@@ -306,7 +326,7 @@ class BirchMurnaghan(EOSBase):
 class PourierTarantola(EOSBase):
     """Pourier-Tarantola EOS."""
 
-    def _func(self, volume, params):
+    def _func(self, volume, params: tuple[float, float, float, float]):
         """Pourier-Tarantola equation from PRB 70, 224107."""
         e0, b0, b1, v0 = tuple(params)
         eta = (volume / v0) ** (1 / 3)
@@ -317,7 +337,7 @@ class PourierTarantola(EOSBase):
 class Vinet(EOSBase):
     """Vinet EOS."""
 
-    def _func(self, volume, params):
+    def _func(self, volume, params: tuple[float, float, float, float]):
         """Vinet equation from PRB 70, 224107."""
         e0, b0, b1, v0 = tuple(params)
         eta = (volume / v0) ** (1 / 3)
@@ -335,7 +355,7 @@ class PolynomialEOS(EOSBase):
     def _func(self, volume, params):
         return np.poly1d(list(params))(volume)
 
-    def fit(self, order):
+    def fit(self, order: int) -> None:
         """
         Do polynomial fitting and set the parameters. Uses numpy polyfit.
 
@@ -345,7 +365,7 @@ class PolynomialEOS(EOSBase):
         self.eos_params = np.polyfit(self.volumes, self.energies, order)
         self._set_params()
 
-    def _set_params(self):
+    def _set_params(self) -> None:
         """
         Use the fit polynomial to compute the parameter e0, b0, b1 and v0
         and set to the _params attribute.
@@ -372,7 +392,7 @@ class DeltaFactor(PolynomialEOS):
         x = volume ** (-2 / 3.0)
         return np.poly1d(list(params))(x)
 
-    def fit(self, order=3):
+    def fit(self, order: int = 3) -> None:
         """Overridden since this eos works with volume**(2/3) instead of volume."""
         x = self.volumes ** (-2 / 3.0)
         self.eos_params = np.polyfit(x, self.energies, order)
@@ -407,7 +427,12 @@ class DeltaFactor(PolynomialEOS):
 class NumericalEOS(PolynomialEOS):
     """A numerical EOS."""
 
-    def fit(self, min_ndata_factor=3, max_poly_order_factor=5, min_poly_order=2):
+    def fit(
+        self,
+        min_ndata_factor: int = 3,
+        max_poly_order_factor: int = 5,
+        min_poly_order: int = 2,
+    ) -> None:
         """Fit the input data to the 'numerical eos', the equation of state employed
         in the quasiharmonic Debye model described in the paper:
         10.1103/PhysRevB.90.174107.
@@ -539,7 +564,7 @@ class EOS:
        eos_fit.plot()
     """
 
-    MODELS: ClassVar = {
+    MODELS: ClassVar[dict[str, Any]] = {
         "murnaghan": Murnaghan,
         "birch": Birch,
         "birch_murnaghan": BirchMurnaghan,
@@ -549,7 +574,7 @@ class EOS:
         "numerical_eos": NumericalEOS,
     }
 
-    def __init__(self, eos_name="murnaghan"):
+    def __init__(self, eos_name: str = "murnaghan") -> None:
         """
         Args:
             eos_name (str): Type of EOS to fit.
@@ -562,7 +587,7 @@ class EOS:
         self._eos_name = eos_name
         self.model = self.MODELS[eos_name]
 
-    def fit(self, volumes, energies):
+    def fit(self, volumes: Sequence[float], energies: Sequence[float]) -> EOSBase:
         """Fit energies as function of volumes.
 
         Args:
diff --git a/src/pymatgen/analysis/prototypes.py b/src/pymatgen/analysis/prototypes.py
index e08e6adaca..5cf9df533d 100644
--- a/src/pymatgen/analysis/prototypes.py
+++ b/src/pymatgen/analysis/prototypes.py
@@ -48,22 +48,27 @@ class AflowPrototypeMatcher:
     https://doi.org/10.1016/j.commatsci.2017.01.017
     """
 
-    def __init__(self, initial_ltol=0.2, initial_stol=0.3, initial_angle_tol=5):
+    def __init__(
+        self,
+        initial_ltol: float = 0.2,
+        initial_stol: float = 0.3,
+        initial_angle_tol: float = 5,
+    ) -> None:
         """
         Tolerances as defined in StructureMatcher. Tolerances will be
         gradually decreased until only a single match is found (if possible).
 
         Args:
-            initial_ltol: fractional length tolerance
-            initial_stol: site tolerance
-            initial_angle_tol: angle tolerance
+            initial_ltol (float): fractional length tolerance.
+            initial_stol (float): site tolerance.
+            initial_angle_tol (float): angle tolerance.
         """
         self.initial_ltol = initial_ltol
         self.initial_stol = initial_stol
         self.initial_angle_tol = initial_angle_tol
 
         # Preprocess AFLOW prototypes
-        self._aflow_prototype_library = []
+        self._aflow_prototype_library: list[tuple[Structure, dict]] = []
         for dct in AFLOW_PROTOTYPE_LIBRARY:
             structure: Structure = dct["snl"].structure
             reduced_structure = self._preprocess_structure(structure)
@@ -73,7 +78,11 @@ class AflowPrototypeMatcher:
     def _preprocess_structure(structure: Structure) -> Structure:
         return structure.get_reduced_structure(reduction_algo="niggli").get_primitive_structure()
 
-    def _match_prototype(self, structure_matcher: StructureMatcher, reduced_structure: Structure):
+    def _match_prototype(
+        self,
+        structure_matcher: StructureMatcher,
+        reduced_structure: Structure,
+    ) -> list[dict]:
         tags = []
         for aflow_reduced_structure, dct in self._aflow_prototype_library:
             # Since both structures are already reduced, we can skip the structure reduction step
@@ -84,7 +93,7 @@ class AflowPrototypeMatcher:
                 tags.append(dct)
         return tags
 
-    def _match_single_prototype(self, structure: Structure):
+    def _match_single_prototype(self, structure: Structure) -> list[dict]:
         sm = StructureMatcher(
             ltol=self.initial_ltol,
             stol=self.initial_stol,
@@ -102,23 +111,23 @@ class AflowPrototypeMatcher:
                 break
         return tags
 
-    def get_prototypes(self, structure: Structure) -> list | None:
+    def get_prototypes(self, structure: Structure) -> list[dict] | None:
         """Get prototype(s) structures for a given input structure. If you use this method in
         your work, please cite the appropriate AFLOW publication:
 
-        Mehl, M. J., Hicks, D., Toher, C., Levy, O., Hanson, R. M., Hart, G., & Curtarolo,
-        S. (2017). The AFLOW library of crystallographic prototypes: part 1. Computational
-        Materials Science, 136, S1-S828. https://doi.org/10.1016/j.commatsci.2017.01.017
+            Mehl, M. J., Hicks, D., Toher, C., Levy, O., Hanson, R. M., Hart, G., & Curtarolo,
+            S. (2017). The AFLOW library of crystallographic prototypes: part 1. Computational
+            Materials Science, 136, S1-S828. https://doi.org/10.1016/j.commatsci.2017.01.017
 
         Args:
-            structure: structure to match
+            structure (Structure): structure to match
 
         Returns:
-            list | None: A list of dicts with keys 'snl' for the matched prototype and
-                'tags', a dict of tags ('mineral', 'strukturbericht' and 'aflow') of that
+            list[dict] | None: A list of dicts with keys "snl" for the matched prototype and
+                "tags", a dict of tags ("mineral", "strukturbericht" and "aflow") of that
                 prototype. This should be a list containing just a single entry, but it is
                 possible a material can match multiple prototypes.
         """
-        tags = self._match_single_prototype(structure)
+        tags: list[dict] = self._match_single_prototype(structure)
 
         return tags or None
diff --git a/src/pymatgen/core/structure.py b/src/pymatgen/core/structure.py
index 99f532dbe1..7193952d03 100644
--- a/src/pymatgen/core/structure.py
+++ b/src/pymatgen/core/structure.py
@@ -4673,9 +4673,8 @@ class Structure(IStructure, collections.abc.MutableSequence):
         the structure in place.
 
         Args:
-            indices (list): List of site indices on which to perform the
-                translation.
-            theta (float): Angle in radians
+            indices (list): Site indices on which to perform the rotation.
+            theta (float): Angle in radians.
             axis (3x1 array): Rotation axis vector.
             anchor (3x1 array): Point of rotation.
             to_unit_cell (bool): Whether new sites are transformed to unit cell
@@ -5294,9 +5293,8 @@ class Molecule(IMolecule, collections.abc.MutableSequence):
         """Rotate specific sites by some angle around vector at anchor.
 
         Args:
-            indices (list): List of site indices on which to perform the
-                translation.
-            theta (float): Angle in radians
+            indices (list): Site indices on which to perform the rotation.
+            theta (float): Angle in radians.
             axis (3x1 array): Rotation axis vector.
             anchor (3x1 array): Point of rotation.
 
diff --git a/src/pymatgen/core/units.py b/src/pymatgen/core/units.py
index 2237cfe461..c34b4fd43d 100644
--- a/src/pymatgen/core/units.py
+++ b/src/pymatgen/core/units.py
@@ -12,7 +12,6 @@ from __future__ import annotations
 
 import collections
 import re
-import warnings
 from collections import defaultdict
 from functools import partial
 from numbers import Number
@@ -310,25 +309,6 @@ class FloatWithUnit(float):
             unit (str | Unit): A unit. e.g. "C".
             unit_type (str): A type of unit. e.g. "charge"
         """
-        # Check deprecated memory unit
-        # TODO: remove after 2025-01-01
-        if unit_type == "memory" and str(unit) in {
-            "Kb",
-            "kb",
-            "Mb",
-            "mb",
-            "Gb",
-            "gb",
-            "Tb",
-            "tb",
-        }:
-            warnings.warn(
-                f"Unit {unit!s} is deprecated, please use {str(unit).upper()} instead",
-                DeprecationWarning,
-                stacklevel=2,
-            )
-            unit = str(unit).upper()
-
         if unit_type is not None and str(unit) not in ALL_UNITS[unit_type]:
             raise UnitError(f"{unit} is not a supported unit for {unit_type}")
 
diff --git a/src/pymatgen/io/vasp/sets.py b/src/pymatgen/io/vasp/sets.py
index d23a861ca3..3958efdd99 100644
--- a/src/pymatgen/io/vasp/sets.py
+++ b/src/pymatgen/io/vasp/sets.py
@@ -2599,23 +2599,91 @@ class MVLRelax52Set(VaspInputSet):
     _valid_potcars: Sequence[str] | None = ("PBE_52", "PBE_54", "PBE_64")
 
 
-class MITNEBSet(VaspInputSet):
+@dataclass
+class MITMDSet(VaspInputSet):
+    """Write a VASP MD run. This DOES NOT do multiple stage runs.
+
+    Args:
+        structure (Structure): Input structure.
+        start_temp (float): Starting temperature.
+        end_temp (float): Final temperature.
+        nsteps (int): Number of time steps for simulations. NSW parameter.
+        time_step (float): The time step for the simulation. The POTIM
+            parameter. Defaults to 2fs.
+        spin_polarized (bool): Whether to do spin polarized calculations.
+            The ISPIN parameter. Defaults to False.
+        **kwargs: Other kwargs supported by VaspInputSet.
+    """
+
+    structure: Structure | None = None
+    start_temp: float = 0.0
+    end_temp: float = 300.0
+    nsteps: int = 1000
+    time_step: float = 2
+    spin_polarized: bool = False
+    CONFIG = MITRelaxSet.CONFIG
+
+    @property
+    def incar_updates(self) -> dict[str, Any]:
+        """Updates to the INCAR config for this calculation type."""
+        # MD default settings
+        return {
+            "TEBEG": self.start_temp,
+            "TEEND": self.end_temp,
+            "NSW": self.nsteps,
+            "EDIFF_PER_ATOM": 0.000001,
+            "LSCALU": False,
+            "LCHARG": False,
+            "LPLANE": False,
+            "LWAVE": True,
+            "ISMEAR": 0,
+            "NELMIN": 4,
+            "LREAL": True,
+            "BMIX": 1,
+            "MAXMIX": 20,
+            "NELM": 500,
+            "NSIM": 4,
+            "ISYM": 0,
+            "ISIF": 0,
+            "IBRION": 0,
+            "NBLOCK": 1,
+            "KBLOCK": 100,
+            "SMASS": 0,
+            "POTIM": self.time_step,
+            "PREC": "Low",
+            "ISPIN": 2 if self.spin_polarized else 1,
+            "LDAU": False,
+            "ENCUT": None,
+        }
+
+    @property
+    def kpoints_updates(self) -> Kpoints:
+        """Updates to the kpoints configuration for this calculation type."""
+        return Kpoints.gamma_automatic()
+
+
+class NEBSet(VaspInputSet):
     """Write NEB inputs.
 
     Note that EDIFF is not on a per atom basis for this input set.
     """
 
-    def __init__(self, structures: list[Structure], unset_encut: bool = False, **kwargs) -> None:
+    def __init__(
+        self, structures: list[Structure], unset_encut: bool = False, parent_set="MPRelaxSet", **kwargs
+    ) -> None:
         """
         Args:
             structures: List of Structure objects.
             unset_encut (bool): Whether to unset ENCUT.
+            parent_set (str): The parent input set to inherit from. Defaults to MPRelaxSet. This should be a string
+                name to support MSONable.
             **kwargs: Other kwargs supported by VaspInputSet.
         """
         if len(structures) < 3:
             raise ValueError(f"You need at least 3 structures for an NEB, got {len(structures)}")
         kwargs["sort_structure"] = False
-        super().__init__(structures[0], MITRelaxSet.CONFIG, **kwargs)
+        self.parent = globals()[parent_set]
+        super().__init__(structures[0], self.parent.CONFIG, **kwargs)
         self.structures = self._process_structures(structures)
 
         self.unset_encut = False
@@ -2634,6 +2702,7 @@ class MITNEBSet(VaspInputSet):
             "LDAU": False,
         }
         self._config_dict["INCAR"].update(defaults)
+        self.parent_set = parent_set
 
     @property
     def poscar(self) -> Poscar:
@@ -2698,7 +2767,7 @@ class MITNEBSet(VaspInputSet):
             if write_cif:
                 poscar.structure.to(filename=str(d / f"{idx}.cif"))
         if write_endpoint_inputs:
-            end_point_param = MITRelaxSet(self.structures[0], user_incar_settings=self.user_incar_settings)
+            end_point_param = self.parent(self.structures[0], user_incar_settings=self.user_incar_settings)
 
             for image in ("00", str(len(self.structures) - 1).zfill(2)):
                 end_point_param.incar.write_file(str(output_dir / image / "INCAR"))
@@ -2715,67 +2784,59 @@ class MITNEBSet(VaspInputSet):
             neb_path.to(filename=f"{output_dir}/path.cif")
 
 
-@dataclass
-class MITMDSet(VaspInputSet):
-    """Write a VASP MD run. This DOES NOT do multiple stage runs.
-
-    Args:
-        structure (Structure): Input structure.
-        start_temp (float): Starting temperature.
-        end_temp (float): Final temperature.
-        nsteps (int): Number of time steps for simulations. NSW parameter.
-        time_step (float): The time step for the simulation. The POTIM
-            parameter. Defaults to 2fs.
-        spin_polarized (bool): Whether to do spin polarized calculations.
-            The ISPIN parameter. Defaults to False.
-        **kwargs: Other kwargs supported by VaspInputSet.
+class CINEBSet(NEBSet):
+    """
+    MAVRL-tested settings for CI-NEB calculations. Note that these parameters
+    requires the VTST modification of VASP from the Henkelman group. See
+    http://theory.cm.utexas.edu/vtsttools/.
     """
 
-    structure: Structure | None = None
-    start_temp: float = 0.0
-    end_temp: float = 300.0
-    nsteps: int = 1000
-    time_step: float = 2
-    spin_polarized: bool = False
-    CONFIG = MITRelaxSet.CONFIG
+    def __init__(self, structures: list[Structure], **kwargs) -> None:
+        r"""
+        Args:
+            structures: Input structures.
+            **kwargs: Keyword args supported by VaspInputSets.
+        """
+        user_incar_settings = kwargs.get("user_incar_settings", {})
 
-    @property
-    def incar_updates(self) -> dict[str, Any]:
-        """Updates to the INCAR config for this calculation type."""
-        # MD default settings
-        return {
-            "TEBEG": self.start_temp,
-            "TEEND": self.end_temp,
-            "NSW": self.nsteps,
-            "EDIFF_PER_ATOM": 0.000001,
-            "LSCALU": False,
-            "LCHARG": False,
-            "LPLANE": False,
-            "LWAVE": True,
+        # CI-NEB settings
+        defaults = {
+            "EDIFF": 5e-5,
+            "EDIFFG": -0.02,
+            "IBRION": 3,
+            "ICHAIN": 0,
+            "IOPT": 1,
+            "ISIF": 2,
             "ISMEAR": 0,
-            "NELMIN": 4,
-            "LREAL": True,
-            "BMIX": 1,
-            "MAXMIX": 20,
-            "NELM": 500,
-            "NSIM": 4,
-            "ISYM": 0,
-            "ISIF": 0,
-            "IBRION": 0,
-            "NBLOCK": 1,
-            "KBLOCK": 100,
-            "SMASS": 0,
-            "POTIM": self.time_step,
-            "PREC": "Low",
-            "ISPIN": 2 if self.spin_polarized else 1,
+            "ISPIN": 2,
+            "LCHARG": False,
+            "LCLIMB": True,
             "LDAU": False,
-            "ENCUT": None,
+            "LORBIT": 0,
+            "NSW": 200,
+            "POTIM": 0,
+            "SPRING": -5,
         }
+        if user_incar_settings != {}:
+            defaults.update(user_incar_settings)
 
-    @property
-    def kpoints_updates(self) -> Kpoints:
-        """Updates to the kpoints configuration for this calculation type."""
-        return Kpoints.gamma_automatic()
+        kwargs["user_incar_settings"] = defaults
+
+        super().__init__(structures, **kwargs)
+
+
+class MITNEBSet(NEBSet):
+    """
+    NEBSet using MITRelaxSet as parent. Retained for compatibility.
+    """
+
+    def __init__(self, structures: list[Structure], **kwargs) -> None:
+        """
+        Args:
+            structures: List of Structure objects.
+            **kwargs: Other kwargs supported by VaspInputSet.
+        """
+        super().__init__(structures, parent_set="MITRelaxSet", **kwargs)
 
 
 @dataclass
diff --git a/src/pymatgen/util/plotly_chempot_layouts.json b/src/pymatgen/util/plotly_chempot_layouts.json
index 92d9032a49..d9efedadad 100644
--- a/src/pymatgen/util/plotly_chempot_layouts.json
+++ b/src/pymatgen/util/plotly_chempot_layouts.json
@@ -55,7 +55,7 @@
     "yanchor": "middle"
   },
   "default_2d_axis_layout": {
-    "titlefont": {
+    "title.font": {
       "size": 24
     },
     "gridcolor": "#dbdbdb",
@@ -68,7 +68,7 @@
     "showline": true
   },
   "default_3d_axis_layout": {
-    "titlefont": {
+    "title.font": {
       "size": 18
     },
     "gridcolor": "#dbdbdb",
diff --git a/src/pymatgen/util/plotly_interface_rxn_layouts.json b/src/pymatgen/util/plotly_interface_rxn_layouts.json
index a1d1fd58a1..72d74c376d 100644
--- a/src/pymatgen/util/plotly_interface_rxn_layouts.json
+++ b/src/pymatgen/util/plotly_interface_rxn_layouts.json
@@ -13,7 +13,7 @@
         "size": 16.0
       },
       "ticks": "inside",
-      "titlefont": {
+      "title.font": {
         "color": "#000000",
         "size": 20.0
       },
@@ -32,7 +32,7 @@
         "size": 16.0
       },
       "ticks": "inside",
-      "titlefont": {
+      "title.font": {
         "color": "#000000",
         "size": 20.0
       },
diff --git a/src/pymatgen/util/plotly_pd_layouts.json b/src/pymatgen/util/plotly_pd_layouts.json
index 8cf5c4bac2..c1e5bb11d2 100644
--- a/src/pymatgen/util/plotly_pd_layouts.json
+++ b/src/pymatgen/util/plotly_pd_layouts.json
@@ -21,7 +21,7 @@
         "size": 16.0
       },
       "ticks": "inside",
-      "titlefont": {
+      "title.font": {
         "color": "#000000",
         "size": 20.0
       },
@@ -54,7 +54,7 @@
         "size": 16.0
       },
       "ticks": "inside",
-      "titlefont": {
+      "title.font": {
         "color": "#000000",
         "size": 20.0
       },
@@ -73,7 +73,7 @@
         "size": 16.0
       },
       "ticks": "inside",
-      "titlefont": {
+      "title.font": {
         "color": "#000000",
         "size": 20.0
       },
diff --git a/src/pymatgen/util/plotting.py b/src/pymatgen/util/plotting.py
index fd983af9e8..0e29b38390 100644
--- a/src/pymatgen/util/plotting.py
+++ b/src/pymatgen/util/plotting.py
@@ -268,8 +268,8 @@ def periodic_table_heatmap(
                 kwargs.setdefault("color_bar", {}).setdefault("title", cbar_label)
                 print('cbar_label is deprecated, use color_bar={"title": cbar_label} instead')
             if cbar_label_size != 14:
-                kwargs.setdefault("color_bar", {}).setdefault("titlefont", {}).setdefault("size", cbar_label_size)
-                print('cbar_label_size is deprecated, use color_bar={"titlefont": {"size": cbar_label_size}} instead')
+                kwargs.setdefault("color_bar", {}).setdefault("title.font", {}).setdefault("size", cbar_label_size)
+                print('cbar_label_size is deprecated, use color_bar={"title.font": {"size": cbar_label_size}} instead')
             if cmap:
                 kwargs.setdefault("colorscale", cmap)
                 print("cmap is deprecated, use colorscale=cmap instead")
diff --git a/src/pymatgen/util/provenance.py b/src/pymatgen/util/provenance.py
index 69220bd52c..7efbec48e6 100644
--- a/src/pymatgen/util/provenance.py
+++ b/src/pymatgen/util/provenance.py
@@ -21,6 +21,7 @@ except ImportError:
 
 if TYPE_CHECKING:
     from collections.abc import Sequence
+    from typing import Any
 
     from typing_extensions import Self
 
@@ -29,23 +30,23 @@ __author__ = "Anubhav Jain, Shyue Ping Ong"
 __credits__ = "Dan Gunter"
 
 
-MAX_HNODE_SIZE = 64_000  # maximum size (bytes) of SNL HistoryNode
-MAX_DATA_SIZE = 256_000  # maximum size (bytes) of SNL data field
-MAX_HNODES = 100  # maximum number of HistoryNodes in SNL file
-MAX_BIBTEX_CHARS = 20_000  # maximum number of characters for BibTeX reference
+MAX_HNODE_SIZE: int = 64_000  # maximum size (bytes) of SNL HistoryNode
+MAX_DATA_SIZE: int = 256_000  # maximum size (bytes) of SNL data field
+MAX_HNODES: int = 100  # maximum number of HistoryNodes in SNL file
+MAX_BIBTEX_CHARS: int = 20_000  # maximum number of characters for BibTeX reference
 
 
 def is_valid_bibtex(reference: str) -> bool:
     """Use pybtex to validate that a reference is in proper BibTeX format.
 
     Args:
-        reference: A String reference in BibTeX format.
+        reference (str): Reference in BibTeX format.
 
     Returns:
-        bool: True if reference is valid bibtex.
+        bool: True if reference is valid BibTeX.
     """
-    # str is necessary since pybtex seems to have an issue with unicode. The
-    # filter expression removes all non-ASCII characters.
+    # str is necessary since pybtex seems to have an issue with unicode.
+    # The filter expression removes all non-ASCII characters.
     str_io = StringIO(reference.encode("ascii", "ignore").decode("ascii"))
     parser = bibtex.Parser()
     errors.set_strict_mode(enable=False)
@@ -62,8 +63,6 @@ class HistoryNode(NamedTuple):
     a custom description of how that code was applied (e.g. a site removal
     Transformation was applied).
 
-    A HistoryNode contains three fields:
-
     Attributes:
         name (str): The name of a code or resource that this Structure encountered in its history.
         url (str): The URL of that code/resource.
@@ -115,16 +114,16 @@ class Author(NamedTuple):
     name: str
     email: str
 
-    def __str__(self):
+    def __str__(self) -> str:
         """String representation of an Author."""
         return f"{self.name} <{self.email}>"
 
-    def as_dict(self):
+    def as_dict(self) -> dict[str, str]:
         """Get MSONable dict."""
         return {"name": self.name, "email": self.email}
 
     @classmethod
-    def from_dict(cls, dct: dict) -> Self:
+    def from_dict(cls, dct: dict[str, str]) -> Self:
         """
         Args:
             dct (dict): Dict representation.
@@ -135,12 +134,17 @@ class Author(NamedTuple):
         return cls(dct["name"], dct["email"])
 
     @classmethod
-    def parse_author(cls, author) -> Self:
+    def parse_author(
+        cls,
+        author: str | dict[str, str] | tuple[str, str],
+    ) -> Self:
         """Parse an Author object from either a String, dict, or tuple.
 
         Args:
-            author: A String formatted as "NAME <email@domain.com>",
-                (name, email) tuple, or a dict with name and email keys.
+            author: in one of the three accepted formats:
+                - A string formatted as "NAME <email@domain.com>".
+                - A (name, email) tuple.
+                - A dict with name and email as keys.
 
         Returns:
             An Author object.
@@ -152,15 +156,18 @@ class Author(NamedTuple):
             if not match or match.start() != 0 or match.end() != len(author):
                 raise ValueError(f"Invalid author format! {author}")
             return cls(match.groups()[0], match.groups()[1])
+
         if isinstance(author, dict):
             return cls.from_dict(author)
+
         if len(author) != 2:
             raise ValueError(f"Invalid author, should be String or (name, email) tuple: {author}")
+
         return cls(author[0], author[1])
 
 
 class StructureNL:
-    """The Structure Notation Language (SNL, pronounced 'snail') is a container for a pymatgen
+    """The Structure Notation Language (SNL, pronounced "snail") is a container for a pymatgen
     Structure/Molecule object with some additional fields for enhanced provenance.
 
     It is meant to be imported/exported in a JSON file format with the following structure:
@@ -178,45 +185,48 @@ class StructureNL:
 
     def __init__(
         self,
-        struct_or_mol,
-        authors,
-        projects=None,
-        references="",
-        remarks=None,
-        data=None,
-        history=None,
-        created_at=None,
-    ):
+        struct_or_mol: Structure | Molecule,
+        authors: str | list[dict] | list[str],
+        projects: list[str] | str | None = None,
+        references: str = "",
+        remarks: list[str] | str | None = None,
+        data: dict | None = None,
+        history: list[dict] | None = None,
+        created_at: datetime | None = None,
+    ) -> None:
         """
         Args:
             struct_or_mol: A pymatgen Structure/Molecule object
-            authors: *List* of {"name":'', "email":''} dicts,
-                *list* of Strings as 'John Doe <johndoe@gmail.com>',
-                or a single String with commas separating authors
-            projects: List of Strings ['Project A', 'Project B']
-            references: A String in BibTeX format
-            remarks: List of Strings ['Remark A', 'Remark B']
-            data: A free form dict. Namespaced at the root level with an
+            authors (list | str):
+                - List of {"name":"", "email":""} dicts.
+                - List of strings as "John Doe <johndoe@gmail.com>".
+                - A single String with commas separating authors.
+            projects (list[str] | str): e.g. ["Project A", "Project B"]
+            references (str): A string in BibTeX format
+            remarks (list[str]): e.g. ["Remark A", "Remark B"]
+            data (dict): A free form dict. Namespaced at the root level with an
                 underscore, e.g. {"_materialsproject": <custom data>}
-            history: List of dicts - [{'name':'', 'url':'', 'description':{}}]
-            created_at: A datetime object.
+            history (list[dict]): e.g. [{"name":"", "url":"", "description":{}}]
+            created_at (datetime): Creation date.
         """
-        # initialize root-level structure keys
+        # Initialize root-level structure keys
         self.structure = struct_or_mol
 
-        # turn authors into list of Author objects
+        # Turn `authors` into list of `Author` objects
         authors = authors.split(",") if isinstance(authors, str) else authors
         self.authors = [Author.parse_author(a) for a in authors]
 
-        # turn projects into list of Strings
+        # Turn `projects` into list of strings
         projects = projects or []
-        self.projects = [projects] if isinstance(projects, str) else projects
+        self.projects: list[str] = [projects] if isinstance(projects, str) else projects
 
-        # check that references are valid BibTeX
+        # Check that references are valid BibTeX
         if not isinstance(references, str):
             raise TypeError("Invalid format for SNL reference! Should be empty string or BibTeX string.")
+
         if references and not is_valid_bibtex(references):
             raise ValueError("Invalid format for SNL reference! Should be BibTeX string.")
+
         if len(references) > MAX_BIBTEX_CHARS:
             raise ValueError(
                 f"The BibTeX string must be fewer than {MAX_BIBTEX_CHARS} chars, you have {len(references)}"
@@ -224,16 +234,16 @@ class StructureNL:
 
         self.references = references
 
-        # turn remarks into list of Strings
+        # Turn `remarks` into list of strings
         remarks = remarks or []
-        self.remarks = [remarks] if isinstance(remarks, str) else remarks
+        self.remarks: list[str] = [remarks] if isinstance(remarks, str) else remarks
 
-        # check remarks limit
+        # Check `remarks` length limit
         for remark in self.remarks:
             if len(remark) > 140:
                 raise ValueError(f"The remark exceeds the maximum size of 140 characters: {len(remark)}")
 
-        # check data limit
+        # Check data length limit
         self.data = data or {}
         if not sys.getsizeof(self.data) < MAX_DATA_SIZE:
             raise ValueError(
@@ -248,7 +258,7 @@ class StructureNL:
                     f"{key=} does not start with an underscore."
                 )
 
-        # check for valid history nodes
+        # Check for valid history nodes
         history = history or []  # initialize null fields
         if len(history) > MAX_HNODES:
             raise ValueError(f"A maximum of {MAX_HNODES} History nodes are supported, you have {len(history)}!")
@@ -258,7 +268,42 @@ class StructureNL:
 
         self.created_at = created_at or f"{datetime.now(tz=timezone.utc):%Y-%m-%d %H:%M:%S.%f%z}"
 
-    def as_dict(self):
+    def __str__(self) -> str:
+        return "\n".join(
+            [
+                f"{key}\n{getattr(self, key)}"
+                for key in (
+                    "structure",
+                    "authors",
+                    "projects",
+                    "references",
+                    "remarks",
+                    "data",
+                    "history",
+                    "created_at",
+                )
+            ]
+        )
+
+    def __eq__(self, other: object) -> bool:
+        """Check for equality between two StructureNL objects."""
+        needed_attrs = (
+            "structure",
+            "authors",
+            "projects",
+            "references",
+            "remarks",
+            "data",
+            "history",
+            "created_at",
+        )
+
+        if not all(hasattr(other, attr) for attr in needed_attrs):
+            return NotImplemented
+
+        return all(getattr(self, attr) == getattr(other, attr) for attr in needed_attrs)
+
+    def as_dict(self) -> dict[str, Any]:
         """Get MSONable dict."""
         dct = self.structure.as_dict()
         dct["@module"] = type(self).__module__
@@ -275,7 +320,7 @@ class StructureNL:
         return dct
 
     @classmethod
-    def from_dict(cls, dct: dict) -> Self:
+    def from_dict(cls, dct: dict[str, Any]) -> Self:
         """
         Args:
             dct (dict): Dict representation.
@@ -305,36 +350,36 @@ class StructureNL:
     def from_structures(
         cls,
         structures: Sequence[Structure],
-        authors: Sequence[dict[str, str]],
-        projects=None,
-        references="",
-        remarks=None,
-        data=None,
-        histories=None,
-        created_at=None,
+        authors,
+        projects: list[str] | None = None,
+        references: str = "",
+        remarks: list[str] | None = None,
+        data: list[dict] | None = None,
+        histories: list[list[dict]] | None = None,
+        created_at: datetime | None = None,
     ) -> list[Self]:
         """A convenience method for getting a list of StructureNL objects by
         specifying structures and metadata separately. Some of the metadata
         is applied to all of the structures for ease of use.
 
         Args:
-            structures: A list of Structure objects
+            structures (Sequence[Structure]): Structure objects
             authors: *List* of {"name":'', "email":''} dicts,
                 *list* of Strings as 'John Doe <johndoe@gmail.com>',
                 or a single String with commas separating authors
-            projects: List of Strings ['Project A', 'Project B']. This
+            projects (list[str]): e.g. ["Project A", "Project B"]. This
                 applies to all structures.
-            references: A String in BibTeX format. Again, this applies to all
+            references (str): A String in BibTeX format. This applies to all
                 structures.
-            remarks: List of Strings ['Remark A', 'Remark B']
-            data: A list of free form dict. Namespaced at the root level
+            remarks (list[str]): e.g. ["Remark A", "Remark B"].
+            data (list[dict]): Free form dicts. Namespaced at the root level
                 with an underscore, e.g. {"_materialsproject":<custom data>}
                 . The length of data should be the same as the list of
                 structures if not None.
-            histories: List of list of dicts - [[{'name':'', 'url':'',
-                'description':{}}], ...] The length of histories should be the
+            histories(list[list[dict]]): e.g. [[{"name":"", "url":"",
+                "description":{}}], ...] The length of histories should be the
                 same as the list of structures if not None.
-            created_at: A datetime object
+            created_at (datetime): Creation date.
         """
         data = [{}] * len(structures) if data is None else data
         histories = [[]] * len(structures) if histories is None else histories
@@ -354,38 +399,3 @@ class StructureNL:
             snl_list.append(snl)
 
         return snl_list
-
-    def __str__(self):
-        return "\n".join(
-            [
-                f"{key}\n{getattr(self, key)}"
-                for key in (
-                    "structure",
-                    "authors",
-                    "projects",
-                    "references",
-                    "remarks",
-                    "data",
-                    "history",
-                    "created_at",
-                )
-            ]
-        )
-
-    def __eq__(self, other: object) -> bool:
-        """Check for equality between two StructureNL objects."""
-        needed_attrs = (
-            "structure",
-            "authors",
-            "projects",
-            "references",
-            "remarks",
-            "data",
-            "history",
-            "created_at",
-        )
-
-        if not all(hasattr(other, attr) for attr in needed_attrs):
-            return NotImplemented
-
-        return all(getattr(self, attr) == getattr(other, attr) for attr in needed_attrs)


Test patch:
diff --git a/tests/core/test_units.py b/tests/core/test_units.py
index c0e2f4ccd4..f4e95e36cd 100644
--- a/tests/core/test_units.py
+++ b/tests/core/test_units.py
@@ -1,7 +1,5 @@
 from __future__ import annotations
 
-import warnings
-
 import pytest
 from numpy.testing import assert_array_equal
 from pytest import approx
@@ -109,17 +107,6 @@ class TestFloatWithUnit(PymatgenTest):
         mega_3 = Memory.from_str("+1.0 MB")
         assert mega_0 == mega_1 == mega_2 == mega_3
 
-    def test_deprecated_memory(self):
-        # TODO: remove after 2025-01-01
-        for unit in ("Kb", "kb", "Mb", "mb", "Gb", "gb", "Tb", "tb"):
-            with pytest.warns(DeprecationWarning, match=f"Unit {unit} is deprecated"):
-                Memory(1, unit)
-
-        with warnings.catch_warnings():
-            warnings.simplefilter("error")
-            for unit in ("KB", "MB", "GB", "TB"):
-                Memory(1, unit)
-
     def test_unitized(self):
         @unitized("eV")
         def func1():
diff --git a/tests/io/vasp/test_sets.py b/tests/io/vasp/test_sets.py
index 0dab414043..ecbd6ae454 100644
--- a/tests/io/vasp/test_sets.py
+++ b/tests/io/vasp/test_sets.py
@@ -22,6 +22,7 @@ from pymatgen.io.vasp.outputs import Vasprun
 from pymatgen.io.vasp.sets import (
     MODULE_DIR,
     BadInputSetWarning,
+    CINEBSet,
     DictSet,
     LobsterSet,
     MatPESStaticSet,
@@ -49,6 +50,7 @@ from pymatgen.io.vasp.sets import (
     MVLRelax52Set,
     MVLScanRelaxSet,
     MVLSlabSet,
+    NEBSet,
     VaspInputGenerator,
     VaspInputSet,
     batch_write_input,
@@ -1311,40 +1313,49 @@ class TestMPMDSet(PymatgenTest):
         assert v.incar["NSW"] == 1000
 
 
-class TestMITNEBSet(PymatgenTest):
+class TestNEBSet(PymatgenTest):
     def setUp(self):
         c1 = [[0.5] * 3, [0.9] * 3]
         c2 = [[0.5] * 3, [0.9, 0.1, 0.1]]
         s1 = Structure(Lattice.cubic(5), ["Si", "Si"], c1)
         s2 = Structure(Lattice.cubic(5), ["Si", "Si"], c2)
         self.structures = [Structure.from_sites(s.sites, to_unit_cell=True) for s in s1.interpolate(s2, 3, pbc=True)]
-        self.vis = MITNEBSet(self.structures)
+        self.vis = NEBSet(self.structures)
+        self.vis_MIT = MITNEBSet(self.structures)
+        self.vis_cineb = CINEBSet(self.structures)
 
     def test_potcar_symbols(self):
         syms = self.vis.potcar_symbols
         assert syms == ["Si"]
 
     def test_unset_encut(self):
-        vis = MITNEBSet(self.structures, unset_encut=True)
+        vis = NEBSet(self.structures, unset_encut=True)
         assert "ENCUT" not in vis.incar
         n_structs = 2
         with pytest.raises(
             ValueError,
             match=f"You need at least 3 structures for an NEB, got {n_structs}",
         ):
-            _ = MITNEBSet(self.structures[:n_structs], unset_encut=True)
+            _ = NEBSet(self.structures[:n_structs], unset_encut=True)
 
     def test_incar(self):
         incar = self.vis.incar
         assert "LDAUU" not in incar
-        assert incar["EDIFF"] == approx(0.00001)
+        assert incar["EDIFF"] == approx(0.00005)
+        assert self.vis_MIT.incar["EDIFF"] == approx(0.00001)
+        assert "LCLIMB" in self.vis_cineb.incar
 
     def test_kpoints(self):
         kpoints = self.vis.kpoints
-        assert kpoints.kpts == [(25,)]
-        assert kpoints.style == Kpoints.supported_modes.Automatic
+        assert kpoints.kpts == [(5, 5, 5)]
+        assert kpoints.style == Kpoints.supported_modes.Gamma
+        assert self.vis_MIT.kpoints.kpts == [(25,)]
+        assert self.vis_MIT.kpoints.style == Kpoints.supported_modes.Automatic
 
     def test_as_from_dict(self):
+        dct = self.vis_MIT.as_dict()
+        v = MontyDecoder().process_decoded(dct)
+        assert v.incar["IMAGES"] == 2
         dct = self.vis.as_dict()
         v = MontyDecoder().process_decoded(dct)
         assert v.incar["IMAGES"] == 2