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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.23] - 2026-09-14

### Fixed
- **`ValidationMixin.validate_positive_float` accepted `NaN` and `inf`.** The
check was `if value < 0`, which both slip past (`nan < 0` is False), so a
non-finite value passed validation and was committed to whatever property was
being set — poisoning every quantity derived from it with no exception
anywhere. Non-finite values now raise `ValueError`. Downstream: any setter
that reads a cached float and scales it (e.g.
`_Electrode.reversible_areal_capacity` in `steer-opencell-design`) could
silently store `nan`.

### Added
- `validate_positive_float(value, name, strictly=False)` takes a `strictly`
flag, mirroring `validate_positive_int`. With `strictly=True` a value of 0.0
is rejected, which callers need when zero would brick the property being set
(a zeroed denominator that can never be scaled back up). The default stays
non-strict (`>= 0`) because many callers legitimately pass 0.0 — zero
insulation width, zero gap, zero electrolyte overfill.

## [0.2.22] - 2026-08-18

### Fixed
Expand Down
29 changes: 25 additions & 4 deletions steer_core/Mixins/TypeChecker.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,20 +135,41 @@ def validate_datum(datum: np.ndarray) -> None:
raise TypeError("All coordinates in datum must be numbers.")

@staticmethod
def validate_positive_float(value: float, name: str) -> None:
def validate_positive_float(value: float, name: str, strictly: bool = False) -> None:
"""Validate that a value is a positive float.

Args:
value: The value to validate.
name: The name of the parameter for error messages.
strictly: If True, value must be strictly positive (> 0). Defaults to
False (>= 0), because many callers legitimately pass 0.0 (zero
insulation width, zero gap, zero overfill). Mirrors
:meth:`validate_positive_int`, which defaults to strict because
its callers are counts.

Raises:
ValueError: If the value is not a positive float.
TypeError: If the value is not a number.
ValueError: If the value is NaN or infinite, or does not meet the
positivity requirement.

Examples:
>>> ValidationMixin.validate_positive_float(5.0, 'width') # OK
>>> ValidationMixin.validate_positive_float(0.0, 'width') # OK (non-strict default)
>>> ValidationMixin.validate_positive_float(0.0, 'width', strictly=True) # ValueError
>>> ValidationMixin.validate_positive_float(-1.0, 'width') # ValueError
>>> ValidationMixin.validate_positive_float(float('nan'), 'width') # ValueError
>>> ValidationMixin.validate_positive_float(float('inf'), 'width') # ValueError
"""
if not isinstance(value, (int, float, np.int64, np.float64)):
raise TypeError(f"{name} must be a number. Provided: {type(value).__name__}.")
if value < 0:
raise ValueError(f"{name} must be a positive float. Provided: {value}.")
if not np.isfinite(value):
raise ValueError(f"{name} must be a finite number. Provided: {value}.")
if strictly:
if value <= 0:
raise ValueError(f"{name} must be a strictly positive float (> 0). Provided: {value}.")
else:
if value < 0:
raise ValueError(f"{name} must be a positive float. Provided: {value}.")

@staticmethod
def validate_positive_int(value: int, name: str, strictly: bool = True) -> None:
Expand Down
2 changes: 1 addition & 1 deletion steer_core/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2024-2026 Stanford University
# SPDX-License-Identifier: AGPL-3.0-or-later

__version__ = "0.2.22"
__version__ = "0.2.23"

from .Mixins.Colors import ColorMixin
from .Mixins.Coordinates import CoordinateMixin
Expand Down
23 changes: 23 additions & 0 deletions test/test_typechecker_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,29 @@ def test_non_numeric_raises(self):
def test_numpy_int64(self):
ValidationMixin.validate_positive_float(np.int64(5), "val")

def test_nan_raises(self):
with pytest.raises(ValueError):
ValidationMixin.validate_positive_float(float("nan"), "val")

def test_infinity_raises(self):
with pytest.raises(ValueError):
ValidationMixin.validate_positive_float(float("inf"), "val")

def test_negative_infinity_raises(self):
with pytest.raises(ValueError):
ValidationMixin.validate_positive_float(float("-inf"), "val")

def test_numpy_nan_raises(self):
with pytest.raises(ValueError):
ValidationMixin.validate_positive_float(np.float64("nan"), "val")

def test_strictly_rejects_zero(self):
with pytest.raises(ValueError):
ValidationMixin.validate_positive_float(0.0, "val", strictly=True)

def test_strictly_accepts_positive(self):
ValidationMixin.validate_positive_float(5.0, "val", strictly=True)


class TestValidatePositiveInt:

Expand Down
Loading