Skip to content

model_sports_heat_stress#281

Merged
FedericoTartarini merged 60 commits intodevelopmentfrom
model_sports_heat_stress
Feb 17, 2026
Merged

model_sports_heat_stress#281
FedericoTartarini merged 60 commits intodevelopmentfrom
model_sports_heat_stress

Conversation

@FedericoTartarini
Copy link
Collaborator

@FedericoTartarini FedericoTartarini commented Feb 3, 2026

Description

Add the Sports Heat Stress Risk model

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

Changes to Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully run tests with your changes locally?

New Feature Submissions:

  1. Does your submission pass tests?
  2. Have you lint your code locally before submission?

Summary by CodeRabbit

  • New Features

    • Added sports heat‑stress risk assessment for outdoor athletics with per‑input risk scores and recommendations.
  • Bug Fixes

    • Tightened dew‑point input validation to enforce valid humidity bounds.
  • Documentation

    • Added sports model docs and references; updated security badge and changelog; added contributor acknowledgement.
  • Tests

    • Comprehensive tests for scalar/array inputs, broadcasting, edge cases and recommendations.
  • Chores

    • Bumped project version and updated build/dev metadata and dependency constraints; minor formatting tweaks.

FedericoTartarini and others added 30 commits October 14, 2025 18:50
Implement a function to assess heat stress risk based on temperature,
humidity, and sport-specific parameters. This includes a comprehensive
dictionary of sports with their respective metabolic rates, clothing
values, and duration, enhancing the model's capability for accurate
risk assessment.
Include notes for implementing documentation and references in the function
docstring to enhance clarity and usability.
build: changed tox docs command for paths with a space
@FedericoTartarini
Copy link
Collaborator Author

FedericoTartarini commented Feb 3, 2026

@yehui-h there is an issue with the code

  result_high = sports_heat_stress_risk(
      tdb=34, tr=34, rh=30, vr=0.5, sport=Sports.RUNNING
  )
print(result_high)

risk_level_interpolated : 2.00
t_medium : 29.10
t_high : 34.00
t_extreme : 39.00
recommendation : 'Increase frequency and/or duration of rest breaks'

but the recommendation is wrong.

Please also check the remaining open comments from Coderabbit

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pythermalcomfort/utilities.py (2)

278-288: ⚠️ Potential issue | 🟠 Major

Guard the dew‑point log domain for 0% RH

With rh == 0, e_s becomes 0 and the np.log calls return -inf, so the function yields non‑finite outputs for an input that currently passes validation. Either reject rh <= 0 or explicitly handle it (e.g., return np.nan).

Proposed fix (reject non‑positive RH)
-    if np.any(rh < 0) or np.any(rh > 100):
-        raise ValueError("Relative humidity must be between 0 and 100%.")
+    if np.any(rh <= 0) or np.any(rh > 100):
+        raise ValueError(
+            "Relative humidity must be in the (0, 100] range for dew‑point calculation."
+        )

As per coding guidelines: Validate domains before logs/roots (arguments must be > 0).


278-280: 🛠️ Refactor suggestion | 🟠 Major

Add type validation for tdb and rh via validate_type

This aligns with repo patterns and ensures a clear TypeError for invalid input types before NumPy coercion.

Proposed addition
-    tdb = np.asarray(tdb, dtype=np.float64)
-    rh = np.asarray(rh, dtype=np.float64)
+    validate_type(tdb, "tdb", (int, float, list, np.ndarray))
+    validate_type(rh, "rh", (int, float, list, np.ndarray))
+    tdb = np.asarray(tdb, dtype=np.float64)
+    rh = np.asarray(rh, dtype=np.float64)

As per coding guidelines: Use validate_type(...) for type checks; raise TypeError for wrong types and ValueError for invalid values.

🤖 Fix all issues with AI agents
In `@pythermalcomfort/__init__.py`:
- Line 7: The package __version__ variable in pythermalcomfort.__init__ is
"3.9.0" but setup.py still declares "3.8.0", causing metadata mismatch; update
setup.py's version to match __version__ (3.9.0) or, better, change setup.py to
read the version from pythermalcomfort.__init__ (e.g., import or read the
__version__ symbol) so both stay synchronized; ensure the unique symbol
__version__ in pythermalcomfort.__init__ and the version value used in setup.py
are identical after your change.

In `@pythermalcomfort/classes_input.py`:
- Around line 1275-1300: Add a proper type hint for the sport parameter in
__init__ (use _SportsValues) and replace the manual isinstance check in
__post_init__ with the shared validate_type call for consistency: import
_SportsValues from pythermalcomfort.models.sports_heat_stress_risk, import
validate_type from the validators module, update the __init__ signature to
include sport: _SportsValues, and in __post_init__ call validate_type(...) to
validate self.sport (so a TypeError is raised on mismatch) instead of the
current isinstance block.

In `@pythermalcomfort/models/sports_heat_stress_risk.py`:
- Around line 24-35: The current type checks for self.clo, self.met, and self.vr
are too strict (they only accept float), so change them to accept numeric types
(e.g., use isinstance(self.clo, (int, float)) or numbers.Real) and then convert
to float before checking positivity; keep duration as a non-negative integer but
broaden its check to integers-like (e.g., isinstance(self.duration,
numbers.Integral) or (int,)) if you want compatibility with numpy ints. Update
the validations for self.clo, self.met, self.vr to cast to float after the
isinstance check and raise ValueError with the same message format if the
numeric value is <= 0; for duration ensure it is integer-like and >= 0 before
raising the ValueError.
- Around line 38-45: The docstring for class Sports contradicts its
implementation: the class is decorated with `@dataclass`(frozen=True) but the
docstring says it's a plain class; remove the `@dataclass`(frozen=True) decorator
from the Sports declaration (or if dataclass behavior is actually required,
instead update the docstring to describe it as a frozen dataclass) — ensure you
modify the class declaration (Sports) accordingly and update the first sentence
of the docstring to match (either “This is a plain class (not a dataclass)…” if
you remove the decorator, or “This is a frozen dataclass…” if you keep it) and
verify no dataclass-specific features are relied upon elsewhere.
- Around line 179-186: The current use of np.vectorize around
_calc_risk_single_value (assigned to vectorized_calc and invoked to produce
risk_levels, t_mediums, t_highs, t_extremes, recommendations) is only a
convenience wrapper and not true vectorisation; update the comment above this
block to explicitly state that np.vectorize performs Python-level looping (no
performance gain) and that the per-element root-finding (brentq) inside
_calc_risk_single_value prevents straightforward NumPy vectorisation, then add a
TODO suggesting options (reworking the algorithm for batch math, using numba, or
parallelizing the scalar calls) so future maintainers know why it’s left scalar
and how to approach optimization.
- Around line 326-339: The current independent clamping of t_medium, t_high,
t_extreme to their min/max ranges can break the invariant t_medium < t_high <
t_extreme and cause negative denominators in the interpolation (see uses of
t_medium, t_high, t_extreme around lines 346–348). After applying the individual
min/max caps, enforce monotonicity by adjusting the three values so they are
strictly increasing (e.g., if t_medium >= t_high, set t_high = max(t_medium +
tiny_delta, min(t_high, max_t_medium)) or alternatively raise t_medium to be
below t_high by a tiny epsilon; similarly ensure t_high < t_extreme), using a
small epsilon to avoid equality; update the block that caps t_medium, t_high,
t_extreme (and reference the min_t_* and max_t_* variables) so the ordering is
restored before any interpolation.

In `@pythermalcomfort/utilities.py`:
- Around line 248-273: Add an "Applicability" section to the dew_point_tmp
docstring describing valid input ranges and limits: state that rh must be within
[0, 100]% (and that a ValueError is raised otherwise), note any temperature
range where the Magnus formulation used in dew_point_tmp is considered valid
(e.g., typical Magnus validity bounds such as roughly -45 °C to +60 °C) and
mention that inputs can be scalars or iterables (as already accepted), include
units for each bound (°C and %) and keep the wording concise following the
existing Args/Returns/Raises/Examples style to align with documentation
standards.

In `@tests/test_sports_heat_stress_risk.py`:
- Around line 206-245: The failing assertion in
test_sports_heat_stress_risk_recommendations expects "Apply active cooling
strategies" for result_high but the sports_heat_stress_risk function returns
"Increase frequency and/or duration of rest breaks"; update the test to assert
the returned recommendation string (change the failing assertion for result_high
to "Increase frequency and/or duration of rest breaks") in the
test_sports_heat_stress_risk_recommendations block referencing result_high and
its recommendation to make the expectation match the implementation.

Comment on lines 1275 to 1300
def __init__(
self,
tdb: float | int | np.ndarray | list,
tr: float | int | np.ndarray | list,
rh: float | int | np.ndarray | list,
vr: float | int | np.ndarray | list,
sport, # Type hint would require importing _SportsValues here
):
# Store sport before calling super().__init__() as it's not a BaseInputs field
self.sport = sport

super().__init__(
tdb=tdb,
tr=tr,
rh=rh,
vr=vr,
)

def __post_init__(self):
# Validate sport is a _SportsValues instance before calling super().__post_init__()
from pythermalcomfort.models.sports_heat_stress_risk import _SportsValues

if not isinstance(self.sport, _SportsValues):
raise TypeError(
"sport must be a _SportsValues instance from the Sports dataclass"
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a sport type hint and use validate_type for consistency.

This keeps the class aligned with the rest of the validation framework and satisfies the typing guideline.

♻️ Suggested fix
-from typing import Any
+from typing import Any, TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from pythermalcomfort.models.sports_heat_stress_risk import _SportsValues
@@
     def __init__(
         self,
         tdb: float | int | np.ndarray | list,
         tr: float | int | np.ndarray | list,
         rh: float | int | np.ndarray | list,
         vr: float | int | np.ndarray | list,
-        sport,  # Type hint would require importing _SportsValues here
+        sport: "_SportsValues",
     ):
@@
-        if not isinstance(self.sport, _SportsValues):
-            raise TypeError(
-                "sport must be a _SportsValues instance from the Sports dataclass"
-            )
+        validate_type(self.sport, "sport", (_SportsValues,))
As per coding guidelines: “Use type hints and NumPy-style docstrings for all library code” and “Use validate_type(...) for type checks; raise TypeError for wrong types.”
🤖 Prompt for AI Agents
In `@pythermalcomfort/classes_input.py` around lines 1275 - 1300, Add a proper
type hint for the sport parameter in __init__ (use _SportsValues) and replace
the manual isinstance check in __post_init__ with the shared validate_type call
for consistency: import _SportsValues from
pythermalcomfort.models.sports_heat_stress_risk, import validate_type from the
validators module, update the __init__ signature to include sport:
_SportsValues, and in __post_init__ call validate_type(...) to validate
self.sport (so a TypeError is raised on mismatch) instead of the current
isinstance block.

Comment on lines +179 to +186
# Vectorize the calculation function to handle arrays
# Returns (risk_level_interpolated, t_medium, t_high, t_extreme, recommendation) for each input
vectorized_calc = np.vectorize(
_calc_risk_single_value, otypes=[float, float, float, float, str]
)
risk_levels, t_mediums, t_highs, t_extremes, recommendations = vectorized_calc(
tdb=tdb, tr=tr, rh=rh, vr=vr, sport=sport
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

np.vectorize is a convenience wrapper, not true vectorisation.

The np.vectorize function is essentially a Python loop with NumPy array dressing—it doesn't provide the performance benefits of actual vectorised NumPy operations. For large input arrays, this will be significantly slower than a truly vectorised implementation.

This is acceptable for correctness, but worth noting if performance becomes a concern with large datasets. The root-finding (brentq) per element is inherently scalar anyway, so true vectorisation would require a different algorithmic approach.

🤖 Prompt for AI Agents
In `@pythermalcomfort/models/sports_heat_stress_risk.py` around lines 179 - 186,
The current use of np.vectorize around _calc_risk_single_value (assigned to
vectorized_calc and invoked to produce risk_levels, t_mediums, t_highs,
t_extremes, recommendations) is only a convenience wrapper and not true
vectorisation; update the comment above this block to explicitly state that
np.vectorize performs Python-level looping (no performance gain) and that the
per-element root-finding (brentq) inside _calc_risk_single_value prevents
straightforward NumPy vectorisation, then add a TODO suggesting options
(reworking the algorithm for batch math, using numba, or parallelizing the
scalar calls) so future maintainers know why it’s left scalar and how to
approach optimization.

Comment on lines +326 to +339
if t_medium > max_t_low:
t_medium = max_t_low
if t_high > max_t_medium:
t_high = max_t_medium
if t_extreme > max_t_high:
t_extreme = max_t_high

# cap the thresholds to the minimum values defined above
if t_extreme < min_t_extreme:
t_extreme = min_t_extreme
if t_high < min_t_high:
t_high = min_t_high
if t_medium < min_t_medium:
t_medium = min_t_medium
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Threshold capping doesn't preserve ordering invariant t_medium < t_high < t_extreme.

The independent capping of each threshold to its own [min, max] range can break the expected ordering. For example, if the solver returns t_extreme below t_medium, after capping you could have t_extreme < t_medium, leading to negative denominators in the interpolation (lines 346-348) and nonsensical risk values.

Consider enforcing monotonicity after capping:

🛡️ Suggested fix to preserve ordering
     # check if the thresholds are within the min and max limits defined above
     if t_medium > max_t_low:
         t_medium = max_t_low
     if t_high > max_t_medium:
         t_high = max_t_medium
     if t_extreme > max_t_high:
         t_extreme = max_t_high

     # cap the thresholds to the minimum values defined above
     if t_extreme < min_t_extreme:
         t_extreme = min_t_extreme
     if t_high < min_t_high:
         t_high = min_t_high
     if t_medium < min_t_medium:
         t_medium = min_t_medium
+
+    # Enforce monotonic ordering: t_medium < t_high < t_extreme
+    if t_high <= t_medium:
+        t_high = t_medium + 0.1
+    if t_extreme <= t_high:
+        t_extreme = t_high + 0.1
🤖 Prompt for AI Agents
In `@pythermalcomfort/models/sports_heat_stress_risk.py` around lines 326 - 339,
The current independent clamping of t_medium, t_high, t_extreme to their min/max
ranges can break the invariant t_medium < t_high < t_extreme and cause negative
denominators in the interpolation (see uses of t_medium, t_high, t_extreme
around lines 346–348). After applying the individual min/max caps, enforce
monotonicity by adjusting the three values so they are strictly increasing
(e.g., if t_medium >= t_high, set t_high = max(t_medium + tiny_delta,
min(t_high, max_t_medium)) or alternatively raise t_medium to be below t_high by
a tiny epsilon; similarly ensure t_high < t_extreme), using a small epsilon to
avoid equality; update the block that caps t_medium, t_high, t_extreme (and
reference the min_t_* and max_t_* variables) so the ordering is restored before
any interpolation.

Comment on lines 248 to +273
"""Calculate the dew point temperature.

The equation use the Magnus formula using the coefficient from
the 2024 edition of the Guide to Instruments and Methods of
Observation. [WMO2024]_.

Parameters
----------
tdb: float or list of floats
dry bulb air temperature, [°C]
rh: float or list of floats
relative humidity, [%]

Returns
-------
dew_point_tmp: ndarray
dew point temperature, [°C]

Example
-------
.. code-block:: python

from pythermalcomfort.utilities import dew_point_tmp
¨
tdb = 25.0 # dry bulb temperature in °C
rh = 60.0 # relative humidity in %
t_d = dew_point_tmp(tdb, rh)
The equation use the Magnus formula using the coefficients from
the 2024 edition of the Guide to Instruments and Methods of
Observation. [WMO2024]_.

Parameters
----------
tdb : float or list of floats
Dry bulb air temperature, [°C]
rh : float or list of floats
Relative humidity, [%]

Returns
-------
dew_point_tmp : ndarray
Dew point temperature, [°C]

Raises
------
ValueError
If relative humidity is outside the range [0, 100]%.

Examples
--------
>>> from pythermalcomfort.utilities import dew_point_tmp
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add an Applicability section to the docstring

Please include an “Applicability” block describing valid ranges (e.g., RH bounds and any temperature constraints) to align with documentation standards.

As per coding guidelines: Ensure docstring sections include Args (with units), Returns, Raises, Examples, Applicability.

🤖 Prompt for AI Agents
In `@pythermalcomfort/utilities.py` around lines 248 - 273, Add an "Applicability"
section to the dew_point_tmp docstring describing valid input ranges and limits:
state that rh must be within [0, 100]% (and that a ValueError is raised
otherwise), note any temperature range where the Magnus formulation used in
dew_point_tmp is considered valid (e.g., typical Magnus validity bounds such as
roughly -45 °C to +60 °C) and mention that inputs can be scalars or iterables
(as already accepted), include units for each bound (°C and %) and keep the
wording concise following the existing Args/Returns/Raises/Examples style to
align with documentation standards.

Comment on lines 206 to 245
def test_sports_heat_stress_risk_recommendations():
"""Test that recommendations are appropriate for different risk levels."""
# Test low risk (risk level < 1.0)
result_low = sports_heat_stress_risk(
tdb=20, tr=20, rh=50, vr=0.5, sport=Sports.RUNNING
)
# Convert numpy array to string for comparison
assert "Increase hydration & modify clothing" == str(result_low.recommendation)
assert result_low.risk_level_interpolated < 1.0

# Test medium risk (risk level 1.0-2.0)
result_medium = sports_heat_stress_risk(
tdb=30, tr=30, rh=50, vr=0.5, sport=Sports.RUNNING
)
assert 1.0 <= result_medium.risk_level_interpolated < 2.0
assert "Increase frequency and/or duration of rest breaks" == str(
result_medium.recommendation
)

# Test high risk (risk level 2.0-3.0)
result_high = sports_heat_stress_risk(
tdb=35, tr=34, rh=30, vr=0.5, sport=Sports.RUNNING
)
assert 2.0 <= result_high.risk_level_interpolated < 3.0
assert "Apply active cooling strategies" == str(result_high.recommendation)

# Test high risk (risk level 2.0-3.0)
result_high = sports_heat_stress_risk(
tdb=34, tr=34, rh=30, vr=0.5, sport=Sports.RUNNING
)
assert 2.0 <= result_high.risk_level_interpolated < 3.0
assert "Apply active cooling strategies" == str(result_high.recommendation)

# Test extreme risk (risk level >= 3.0)
result_extreme = sports_heat_stress_risk(
tdb=50, tr=50, rh=50, vr=0.5, sport=Sports.RUNNING
)
assert "Consider suspending play" in str(result_extreme.recommendation)
assert result_extreme.risk_level_interpolated == pytest.approx(3.0, rel=1e-3)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix failing recommendation expectation (CI).

The pipeline shows this test expecting “Apply active cooling strategies” but the model returns “Increase frequency and/or duration of rest breaks”, so the assertion currently fails. Align the expectation or adjust inputs to reliably land in the high‑risk band.

🔧 Minimal fix (align expectation)
-    assert "Apply active cooling strategies" == str(result_high.recommendation)
+    assert "Increase frequency and/or duration of rest breaks" == str(
+        result_high.recommendation
+    )
🧰 Tools
🪛 GitHub Actions: Pull Request Workflow

[error] 237-237: Test failed: expected recommendation 'Apply active cooling strategies' but got 'Increase frequency and/or duration of rest breaks'. Command: pytest --cov --cov-report=term-missing -vv tests

🤖 Prompt for AI Agents
In `@tests/test_sports_heat_stress_risk.py` around lines 206 - 245, The failing
assertion in test_sports_heat_stress_risk_recommendations expects "Apply active
cooling strategies" for result_high but the sports_heat_stress_risk function
returns "Increase frequency and/or duration of rest breaks"; update the test to
assert the returned recommendation string (change the failing assertion for
result_high to "Increase frequency and/or duration of rest breaks") in the
test_sports_heat_stress_risk_recommendations block referencing result_high and
its recommendation to make the expectation match the implementation.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@Pipfile`:
- Around line 27-29: Update the Pipfile dev-dependencies to remove wildcards and
pin or constrain the three packages: change the pytest entry to a constrained
range (e.g., use a post-fix like pytest = "~=9.0" or a specific patched version
once available), set filelock to ">=3.20.1" to avoid older vulnerable releases,
and set requests to ">=2.32.4"; edit the entries for pytest, filelock, and
requests in the Pipfile accordingly and run pipenv lock to regenerate the
lockfile so CI uses the fixed versions.

Pipfile Outdated
Comment on lines 27 to 29
pytest = "*"
filelock = "*"
requests = "*"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

latest stable versions pytest filelock requests Python 3.12 2025 2026

💡 Result:

  • pytest: 9.0.2 (uploaded Dec 6, 2025) [1]
  • filelock: 3.20.3 (uploaded Jan 9, 2026) [2]
  • requests: 2.32.5 (uploaded Aug 18, 2025) [3]

Sources: [1] pytest on PyPI, [2] filelock on PyPI, [3] requests on PyPI.


🌐 Web query:

pytest security advisories CVE 2025

💡 Result:

Upstream pytest CVEs with a CVE-2025-** ID (as of Feb 3, 2026)

CVE-2025-71176 — predictable /tmp/pytest-of-{user} temp directory (UNIX)

  • Impact: local users may trigger denial of service and potentially privilege-related impact due to predictable temp dir naming and unsafe handling (symlink/TOCTOU risk). [1][4]
  • Affected: pytest through 9.0.2 on UNIX. [1][2]
  • Published in NVD/OSV: Jan 22, 2026 (note: CVE ID is “2025”, but publication is 2026). [1][2]
  • Fix: upgrade to a version later than 9.0.2. [2]
  • Mitigation/workaround (until upgraded): use a securely-created temp dir (e.g., mktemp -d) and point tempfile/pytest at it via environment variables (incl. PYTEST_DEBUG_TEMPROOT), and consider enabling Linux fs.protected_* sysctls. [3]

Sources: NVD [1], OSV [2], oss-security post [3], upstream issue [4].


🌐 Web query:

filelock security advisories CVE 2025

💡 Result:

filelock (Python) — 2025 security advisories / CVEs

CVE-2025-68146 (GHSA-w853-jp5j-5j7f)

  • Issue: Time-of-Check-Time-of-Use (TOCTOU) race condition in lock-file creation that can enable local symlink/reparse-point attacks, leading to corruption or truncation of arbitrary files. [1][2]
  • Affected versions: filelock < 3.20.1. [1][2]
  • Fixed version: 3.20.1 (PyPI release date shown as Dec 16, 2025 in some trackers). [2][3]
  • Mitigations (if you can’t upgrade immediately): Consider using SoftFileLock and restrict/monitor lock-file directories (partial mitigation). [2]
  • Downstream/vendor bulletins: Example: IBM published a bulletin noting impact via products that use filelock (IBM Netezza Appliance) and references CVE-2025-68146. [4]

Current PyPI status (for upgrade planning)

  • PyPI shows filelock releases beyond the fix (e.g., 3.20.3 uploaded Jan 9, 2026). [3]

Sources: NVD CVE record [1], OSV/GHSA advisory entry [2], PyPI project page [3], IBM Security Bulletin [4].


🌐 Web query:

requests library security advisories CVE 2025

💡 Result:

As of 2025, the main Requests (Python requests) security advisory activity is not for a CVE with a 2025 ID, but for CVE-2024-47081 that was published June 9, 2025:

  • CVE-2024-47081 (published Jun 9, 2025): .netrc credential leak via maliciously crafted URLs — affects requests < 2.32.4; fixed in 2.32.4. Workaround for older versions: disable .netrc usage with trust_env=False on the Session. [1]
  • Linux vendor advisories in 2025 (example): SUSE shipped an update on June 18, 2025 addressing CVE-2024-47081 in their python-requests package. [2]
  • Other 2025 security bulletins (example): IBM notes the same issue and recommends upgrading to 2.32.4. [3]

Bottom line: if you’re looking for “Requests advisories in 2025”, the key one is published in 2025 but is CVE-2024-47081; upgrade to requests>=2.32.4. [1]

References: [1] GitHub Advisory Database (GHSA-9hjg-9r4m-mvj7 / CVE-2024-47081), [2] SUSE security announcement SUSE-SU-2025:01998-1, [3] IBM AIX/VIOS Python security bulletin.


Pin or constrain these dev dependency versions immediately—security vulnerabilities are present.

Using * exposes your project to known security issues:

  • pytest: CVE-2025-71176 affects all released versions including 9.0.2—local privilege escalation and DoS via predictable temporary directories on UNIX systems. Constraint to a patched version once released, or apply mitigation steps.
  • filelock: CVE-2025-68146 (TOCTOU race condition enabling symlink attacks and arbitrary file corruption) affected versions prior to 3.20.1. Pin to >= 3.20.1.
  • requests: CVE-2024-47081 (.netrc credential leak) affected versions prior to 2.32.4. Pin to >= 2.32.4.

Recommend pinning to specific versions or compatible ranges (e.g., pytest = "~=9.0" once a fix is available, filelock = ">=3.20.1", requests = ">=2.32.4") to ensure CI reproducibility and prevent unexpected breaking changes.

🤖 Prompt for AI Agents
In `@Pipfile` around lines 27 - 29, Update the Pipfile dev-dependencies to remove
wildcards and pin or constrain the three packages: change the pytest entry to a
constrained range (e.g., use a post-fix like pytest = "~=9.0" or a specific
patched version once available), set filelock to ">=3.20.1" to avoid older
vulnerable releases, and set requests to ">=2.32.4"; edit the entries for
pytest, filelock, and requests in the Pipfile accordingly and run pipenv lock to
regenerate the lockfile so CI uses the fixed versions.

yehui-h and others added 7 commits February 4, 2026 09:41
…and security fixes

- Fix security vulnerabilities: CVE-2025-71176 (pytest), CVE-2025-68146 (filelock), CVE-2024-47081 (requests)
- Update dependencies: numpy 2.3.5, scipy 1.17.0, numba 0.63.1, black 26.1.0
- Improve type validation: allow int/float for clo/met/vr parameters
- Add monotonic ordering enforcement for temperature thresholds
- Enhance risk level calculation with better boundary condition handling
- Use TYPE_CHECKING for proper type hints without circular imports
- Bump version to 3.9.0
- Replace type checks with a centralized validation function for clo, met, vr, and duration parameters.
- Ensure that clo, met, and vr are positive values, and duration is a non-negative integer.
…culation

Remove forced threshold adjustments and simplify boundary conditions for more accurate risk assessment.
Clean up formatting, type hints, error messages, and documentation examples
feat: enhance sports heat stress risk model with improved validation …
Copilot AI review requested due to automatic review settings February 11, 2026 00:18
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
setup.py (1)

100-106: ⚠️ Potential issue | 🟡 Minor

Bump numpy lower bound to avoid versions incompatible with Python 3.10.

numpy>=1.21 allows NumPy 1.21.0–1.21.2, which don't officially support Python 3.10. Since the package requires Python ≥3.10.0, the constraint should be at least numpy>=1.21.3 (which introduced official Python 3.10 support). Alternatively, numpy>=1.22 is also acceptable and more conservative.

Suggested fix
-        "numpy>=1.21,<2.3",
+        "numpy>=1.22,<2.3",
🤖 Fix all issues with AI agents
In `@pythermalcomfort/models/sports_heat_stress_risk.py`:
- Around line 376-397: The recommendation thresholds in _get_recommendation are
based on the rounded risk_level which can shift tier boundaries (e.g., 0.95
rounds to 1.0 and maps to the next tier); add a short clarifying comment at the
call site where the risk value is rounded (the place that calls
_get_recommendation after using round(...)) stating that recommendations
intentionally use the rounded value to determine tiers, so boundary values are
assigned to the higher tier.
- Around line 39-41: The duration validation in _SportsValues.__post_init__
currently allows duration == 0 which later causes a ZeroDivisionError in
_calc_risk_single_value where it divides by sport.duration; change the
validation to require duration > 0 (strictly positive) and raise a ValueError
with a clear message like "duration must be a positive integer > 0, got
{self.duration}" so downstream code (e.g., the division in
_calc_risk_single_value) cannot encounter division-by-zero.
- Around line 258-276: The sweat-loss normalization in
calculate_threshold_water_loss uses a magic constant 45.0 (sl_scalar /
float(sport.duration) * 45.0) with no explanation; replace this hardcoded number
with a clearly named constant (e.g., REFERENCE_DURATION_MINUTES or
DEFAULT_COMPARISON_DURATION) declared at module scope or make it a configurable
parameter to calculate_threshold_water_loss, update the computation to use that
symbol instead of 45.0, and add a brief comment explaining that this value is
the reference comparison window (minutes) used to normalize sweat loss across
sports; ensure references to sport.duration and sweat_loss_g remain unchanged.

In `@tests/test_sports_heat_stress_risk.py`:
- Around line 225-237: The test assumes sports_heat_stress_risk returns a
risk_level_interpolated >= 2.0 and recommendation "Apply active cooling
strategies" for inputs (tdb=34, tr=34, rh=30, vr=0.5, sport=Sports.RUNNING), but
due to threshold ordering/monotonicity bug the computed score can be just under
2.0 causing recommendation mismatch; fix the underlying monotonicity/threshold
logic in sports_heat_stress_risk (and any threshold arrays or comparator used to
map raw score → risk_level_interpolated/recommendation) so that values at or
above the intended boundary consistently map to risk level 2.0 and yield the
"Apply active cooling strategies" recommendation, then re-run the test to
confirm risk_level_interpolated and recommendation are stable for the given
inputs.
- Around line 269-297: The minimum-threshold assertions in
test_sports_heat_stress_risk_threshold_capping are too low and must match the
model constants; update the checks that reference result_low.t_medium,
result_low.t_high, and result_low.t_extreme to assert >= 23.0, >= 25.0, and >=
26.0 respectively (these correspond to the model's min_t_medium, min_t_high,
min_t_extreme used by sports_heat_stress_risk), and adjust the inline comment to
reflect the actual constant values.

Comment on lines +39 to +41
if self.duration < 0:
msg = f"duration must be a non-negative integer >= 0, got {self.duration}"
raise ValueError(msg)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

duration=0 passes validation but causes ZeroDivisionError downstream.

_SportsValues.__post_init__ allows duration >= 0, but _calc_risk_single_value at line 276 divides by sport.duration:

sl_scalar / float(sport.duration) * 45.0 - float(sweat_loss_g)

If duration is 0, this will raise a ZeroDivisionError at runtime. The validation should require duration > 0.

Proposed fix
-        if self.duration < 0:
-            msg = f"duration must be a non-negative integer >= 0, got {self.duration}"
+        if self.duration <= 0:
+            msg = f"duration must be a positive integer > 0, got {self.duration}"
             raise ValueError(msg)

As per coding guidelines: "Enforce physical constraints in validation (e.g., non-negativity) with clear error messages."

🤖 Prompt for AI Agents
In `@pythermalcomfort/models/sports_heat_stress_risk.py` around lines 39 - 41, The
duration validation in _SportsValues.__post_init__ currently allows duration ==
0 which later causes a ZeroDivisionError in _calc_risk_single_value where it
divides by sport.duration; change the validation to require duration > 0
(strictly positive) and raise a ValueError with a clear message like "duration
must be a positive integer > 0, got {self.duration}" so downstream code (e.g.,
the division in _calc_risk_single_value) cannot encounter division-by-zero.

Comment on lines +258 to +276
def calculate_threshold_water_loss(x):
sl = phs(
tdb=x,
tr=tr,
v=vr,
rh=rh,
met=sport.met,
clo=sport.clo,
posture="standing",
duration=sport.duration,
round_output=False,
limit_inputs=False,
acclimatized=100,
i_mst=0.4,
).sweat_loss_g

# Ensure a scalar float is returned for the root solver
sl_scalar = float(np.asarray(sl))
return float(sl_scalar / float(sport.duration) * 45.0 - float(sweat_loss_g))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Sweat-loss normalisation has an implicit assumption that should be documented.

Line 276 normalises sweat loss to a 45-minute window: sl_scalar / float(sport.duration) * 45.0. The magic number 45.0 is unexplained — presumably a reference period for comparing across sports with different durations. This should be extracted to a named constant or documented in a comment so future maintainers understand the rationale.

Proposed improvement
+    # Reference period (minutes) used to normalise sweat loss across sports
+    # with different activity durations, so thresholds are comparable.
+    _REFERENCE_PERIOD_MIN = 45.0
+
     def calculate_threshold_water_loss(x):
         ...
-        return float(sl_scalar / float(sport.duration) * 45.0 - float(sweat_loss_g))
+        return float(sl_scalar / float(sport.duration) * _REFERENCE_PERIOD_MIN - float(sweat_loss_g))

Based on learnings: FedericoTartarini prefers configurable parameters over hardcoded values in utility functions.

🤖 Prompt for AI Agents
In `@pythermalcomfort/models/sports_heat_stress_risk.py` around lines 258 - 276,
The sweat-loss normalization in calculate_threshold_water_loss uses a magic
constant 45.0 (sl_scalar / float(sport.duration) * 45.0) with no explanation;
replace this hardcoded number with a clearly named constant (e.g.,
REFERENCE_DURATION_MINUTES or DEFAULT_COMPARISON_DURATION) declared at module
scope or make it a configurable parameter to calculate_threshold_water_loss,
update the computation to use that symbol instead of 45.0, and add a brief
comment explaining that this value is the reference comparison window (minutes)
used to normalize sweat loss across sports; ensure references to sport.duration
and sweat_loss_g remain unchanged.

Comment on lines +376 to +397
def _get_recommendation(risk_level: float) -> str:
"""Get heat stress management recommendations based on risk level.

Parameters
----------
risk_level : float
Interpolated risk level (0.0-3.0).

Returns
-------
str
Evidence-based recommendation text for managing heat stress at the given
risk level.
"""
if risk_level < 1.0:
return "Increase hydration & modify clothing"
elif risk_level < 2.0:
return "Increase frequency and/or duration of rest breaks"
elif risk_level < 3.0:
return "Apply active cooling strategies"
else:
return "Consider suspending play"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

_get_recommendation thresholds use < comparisons on rounded values — boundary alignment is fragile.

A risk level of exactly 1.0 returns "Increase frequency…" (correct per elif risk_level < 2.0), but the rounding at line 362 means values like 0.95 round to 1.0 and shift recommendation tier. This is technically correct but worth a brief comment at the call site (line 365) explaining that recommendations are intentionally based on the rounded value.

🤖 Prompt for AI Agents
In `@pythermalcomfort/models/sports_heat_stress_risk.py` around lines 376 - 397,
The recommendation thresholds in _get_recommendation are based on the rounded
risk_level which can shift tier boundaries (e.g., 0.95 rounds to 1.0 and maps to
the next tier); add a short clarifying comment at the call site where the risk
value is rounded (the place that calls _get_recommendation after using
round(...)) stating that recommendations intentionally use the rounded value to
determine tiers, so boundary values are assigned to the higher tier.

Comment on lines 225 to 237
# Test high risk (risk level 2.0-3.0)
result_high = sports_heat_stress_risk(
tdb=35, tr=34, rh=30, vr=0.5, sport=Sports.RUNNING
)
assert 2.0 <= result_high.risk_level_interpolated < 3.0
assert "Apply active cooling strategies" == str(result_high.recommendation)

# Test high risk (risk level 2.0-3.0)
result_high = sports_heat_stress_risk(
tdb=34, tr=34, rh=30, vr=0.5, sport=Sports.RUNNING
)
assert 2.0 <= result_high.risk_level_interpolated < 3.0
assert "Apply active cooling strategies" == str(result_high.recommendation)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

These assertions are likely to fail — matches the bug reported by Federico in PR comments.

The PR author noted that sports_heat_stress_risk(tdb=34, tr=34, rh=30, vr=0.5, sport=Sports.RUNNING) returns risk_level_interpolated: 2.00 with recommendation "Increase frequency and/or duration of rest breaks", not "Apply active cooling strategies". If the underlying risk level before rounding is slightly below 2.0 (e.g., 1.95 → rounds to 2.0), the assertion at line 229 (2.0 <= ...) would fail.

This is likely a symptom of the threshold ordering issue flagged in the model review. Once the monotonicity fix is applied, re-verify these expected values.

🤖 Prompt for AI Agents
In `@tests/test_sports_heat_stress_risk.py` around lines 225 - 237, The test
assumes sports_heat_stress_risk returns a risk_level_interpolated >= 2.0 and
recommendation "Apply active cooling strategies" for inputs (tdb=34, tr=34,
rh=30, vr=0.5, sport=Sports.RUNNING), but due to threshold ordering/monotonicity
bug the computed score can be just under 2.0 causing recommendation mismatch;
fix the underlying monotonicity/threshold logic in sports_heat_stress_risk (and
any threshold arrays or comparator used to map raw score →
risk_level_interpolated/recommendation) so that values at or above the intended
boundary consistently map to risk level 2.0 and yield the "Apply active cooling
strategies" recommendation, then re-run the test to confirm
risk_level_interpolated and recommendation are stable for the given inputs.

Comment on lines +269 to +297
def test_sports_heat_stress_risk_threshold_capping():
"""Test that threshold temperatures are properly capped at min/max bounds."""
# Test conditions that should trigger maximum threshold capping
# Very high humidity and temperature should cap thresholds
result_high = sports_heat_stress_risk(
tdb=45, tr=45, rh=90, vr=0.1, sport=Sports.RUNNING
)

# Verify thresholds respect maximum bounds (from implementation)
# max_t_low = 34.5, max_t_medium = 39, max_t_high = 43.5
assert result_high.t_medium <= 34.5
assert result_high.t_high <= 39.0
assert result_high.t_extreme <= 43.5

# Test conditions that should trigger minimum threshold capping
# Low humidity and low temperature should set minimum thresholds
result_low = sports_heat_stress_risk(
tdb=25, tr=25, rh=10, vr=2.0, sport=Sports.WALKING
)

# Verify thresholds respect minimum bounds (from implementation)
# min_t_medium = 22, min_t_high = 23, min_t_extreme = 25
assert result_low.t_medium >= 22.0
assert result_low.t_high >= 23.0
assert result_low.t_extreme >= 25.0

# Verify threshold ordering is always maintained
assert result_high.t_medium < result_high.t_high < result_high.t_extreme
assert result_low.t_medium < result_low.t_high < result_low.t_extreme
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Threshold minimum assertions are too lenient — won't catch regressions.

The model defines min_t_medium=23, min_t_high=25, min_t_extreme=26, but the assertions check against 22.0, 23.0, and 25.0 respectively. These will always pass even if the capping logic regresses.

Proposed fix — align with actual model constants
-    assert result_low.t_medium >= 22.0
-    assert result_low.t_high >= 23.0
-    assert result_low.t_extreme >= 25.0
+    assert result_low.t_medium >= 23.0   # min_t_medium in model
+    assert result_low.t_high >= 25.0     # min_t_high in model
+    assert result_low.t_extreme >= 26.0  # min_t_extreme in model
🤖 Prompt for AI Agents
In `@tests/test_sports_heat_stress_risk.py` around lines 269 - 297, The
minimum-threshold assertions in test_sports_heat_stress_risk_threshold_capping
are too low and must match the model constants; update the checks that reference
result_low.t_medium, result_low.t_high, and result_low.t_extreme to assert >=
23.0, >= 25.0, and >= 26.0 respectively (these correspond to the model's
min_t_medium, min_t_high, min_t_extreme used by sports_heat_stress_risk), and
adjust the inline comment to reflect the actual constant values.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Sports Heat Stress Risk model to pythermalcomfort, exposing it via the public models API and documenting/testing it alongside existing indices and models.

Changes:

  • Introduces sports_heat_stress_risk() (PHS-backed) plus a Sports namespace of sport presets.
  • Adds new input/return dataclasses for validation and results (SportsHeatStressInputs, SportsHeatStressRisk).
  • Updates docs, references, tests, and bumps version/dependency constraints.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
pythermalcomfort/models/sports_heat_stress_risk.py New sports heat stress risk model implementation and sport presets.
pythermalcomfort/classes_input.py Adds SportsHeatStressInputs for validation/broadcasting checks.
pythermalcomfort/classes_return.py Adds SportsHeatStressRisk return dataclass.
pythermalcomfort/models/__init__.py Exposes Sports and sports_heat_stress_risk via public API.
pythermalcomfort/utilities.py Updates dew point docstring and enforces RH bounds.
tests/test_sports_heat_stress_risk.py New test suite covering scalar/array/broadcasting/edge cases.
docs/documentation/models.rst Documents the new model and associated classes.
docs/documentation/references.rst Adds references for the sports heat policy/tool.
setup.py Version bump and NumPy requirement constraint update.
pythermalcomfort/__init__.py Updates package __version__.
docs/conf.py Updates documented version/release.
CHANGELOG.rst Adds 3.9.0 entry describing the new model.
README.rst Updates the security/package-health badge URL/target.
Pipfile Adjusts dev dependency constraints for security advisories.
AUTHORS.rst Adds a contributor entry.
.bumpversion.toml Bumps current_version to 3.9.0.

Comment on lines +1055 to +1072
risk_level_interpolated : float or list of floats
Interpolated risk level (0.0-3.0), [dimensionless].
Risk levels: 0-1 = low, 1-2 = moderate, 2-3 = high, 3+ = extreme.
t_medium : float or list of floats
Temperature threshold for medium risk level, [°C].
t_high : float or list of floats
Temperature threshold for high risk level, [°C].
t_extreme : float or list of floats
Temperature threshold for extreme risk level, [°C].
recommendation : str or list of str
Heat stress management recommendations for the calculated risk level.
"""

risk_level_interpolated: float | list[float]
t_medium: float | list[float]
t_high: float | list[float]
t_extreme: float | list[float]
recommendation: str | list[str]
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return fields are typed as float | list[...], but sports_heat_stress_risk() returns NumPy arrays (from np.vectorize) for both scalar and vector inputs. Please update the type hints to include np.ndarray/npt.NDArray (or npt.ArrayLike) so the public API typing matches runtime behavior.

Suggested change
risk_level_interpolated : float or list of floats
Interpolated risk level (0.0-3.0), [dimensionless].
Risk levels: 0-1 = low, 1-2 = moderate, 2-3 = high, 3+ = extreme.
t_medium : float or list of floats
Temperature threshold for medium risk level, [°C].
t_high : float or list of floats
Temperature threshold for high risk level, [°C].
t_extreme : float or list of floats
Temperature threshold for extreme risk level, [°C].
recommendation : str or list of str
Heat stress management recommendations for the calculated risk level.
"""
risk_level_interpolated: float | list[float]
t_medium: float | list[float]
t_high: float | list[float]
t_extreme: float | list[float]
recommendation: str | list[str]
risk_level_interpolated : numpy.ndarray or float or list of floats
Interpolated risk level (0.0-3.0), [dimensionless].
Risk levels: 0-1 = low, 1-2 = moderate, 2-3 = high, 3+ = extreme.
t_medium : numpy.ndarray or float or list of floats
Temperature threshold for medium risk level, [°C].
t_high : numpy.ndarray or float or list of floats
Temperature threshold for high risk level, [°C].
t_extreme : numpy.ndarray or float or list of floats
Temperature threshold for extreme risk level, [°C].
recommendation : numpy.ndarray or str or list of str
Heat stress management recommendations for the calculated risk level.
"""
risk_level_interpolated: npt.ArrayLike
t_medium: npt.ArrayLike
t_high: npt.ArrayLike
t_extreme: npt.ArrayLike
recommendation: npt.ArrayLike

Copilot uses AI. Check for mistakes.
Comment on lines +701 to +725
"""Test that infinite values are handled or rejected appropriately."""
# Positive infinity should likely cause issues in the model
# This tests robustness - behavior may vary depending on implementation
try:
result = sports_heat_stress_risk(
tdb=np.inf, tr=30, rh=50, vr=0.5, sport=Sports.RUNNING
)
# If it doesn't raise an error, ensure result is still valid
assert isinstance(result, SportsHeatStressRisk)
except (ValueError, RuntimeError, Warning):
# It's acceptable to raise an error for infinity
pass

# Negative infinity
try:
result = sports_heat_stress_risk(
tdb=-np.inf, tr=30, rh=50, vr=0.5, sport=Sports.WALKING
)
# Should treat as extreme cold (risk = 0)
if isinstance(result, SportsHeatStressRisk):
risk_value = float(np.asarray(result.risk_level_interpolated).item())
assert risk_value == pytest.approx(0.0, abs=0.01)
except (ValueError, RuntimeError, Warning):
# It's acceptable to raise an error for infinity
pass
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses a try/except that swallows failures (including Warning) and will pass even if the function misbehaves. Prefer explicit assertions (e.g., pytest.raises(...) for invalid infinities, or assert on a defined output contract) so regressions are actually caught.

Suggested change
"""Test that infinite values are handled or rejected appropriately."""
# Positive infinity should likely cause issues in the model
# This tests robustness - behavior may vary depending on implementation
try:
result = sports_heat_stress_risk(
tdb=np.inf, tr=30, rh=50, vr=0.5, sport=Sports.RUNNING
)
# If it doesn't raise an error, ensure result is still valid
assert isinstance(result, SportsHeatStressRisk)
except (ValueError, RuntimeError, Warning):
# It's acceptable to raise an error for infinity
pass
# Negative infinity
try:
result = sports_heat_stress_risk(
tdb=-np.inf, tr=30, rh=50, vr=0.5, sport=Sports.WALKING
)
# Should treat as extreme cold (risk = 0)
if isinstance(result, SportsHeatStressRisk):
risk_value = float(np.asarray(result.risk_level_interpolated).item())
assert risk_value == pytest.approx(0.0, abs=0.01)
except (ValueError, RuntimeError, Warning):
# It's acceptable to raise an error for infinity
pass
"""Test that infinite dry-bulb temperatures are rejected."""
# Positive infinity should be treated as invalid input
with pytest.raises((ValueError, RuntimeError, Warning)):
sports_heat_stress_risk(
tdb=np.inf,
tr=30,
rh=50,
vr=0.5,
sport=Sports.RUNNING,
)
# Negative infinity should also be treated as invalid input
with pytest.raises((ValueError, RuntimeError, Warning)):
sports_heat_stress_risk(
tdb=-np.inf,
tr=30,
rh=50,
vr=0.5,
sport=Sports.WALKING,
)

Copilot uses AI. Check for mistakes.
Comment on lines +220 to +221
assert 1.0 <= result_medium.risk_level_interpolated < 2.0
assert "Increase frequency and/or duration of rest breaks" == str(
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion compares risk_level_interpolated directly to floats; if it is a 0-d NumPy array, chained comparisons can raise an ambiguous truth-value error. Convert to a Python scalar (e.g., float(np.asarray(...).item())) before doing range comparisons.

Copilot uses AI. Check for mistakes.
Comment on lines +250 to +252
The equation use the Magnus formula using the coefficients from
the 2024 edition of the Guide to Instruments and Methods of
Observation. [WMO2024]_.
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar: "The equation use" should be "The equation uses".

Suggested change
The equation use the Magnus formula using the coefficients from
the 2024 edition of the Guide to Instruments and Methods of
Observation. [WMO2024]_.
The equation uses the Magnus formula with coefficients from the
2024 edition of the Guide to Instruments and Methods of
Observation [WMO2024]_.

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +51
Use attributes like `Sports.RUNNING` to obtain a `_SportsValues` instance.
This class uses a frozen dataclass decorator to prevent modification of the
namespace. Attributes are class-level constants, not instance fields.
"""
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sports is documented as a frozen namespace to prevent modification, but the class attributes are _SportsValues instances that are still mutable (e.g., Sports.RUNNING.met can be reassigned). If immutability is intended, make _SportsValues frozen=True (or otherwise immutable) and adjust the docstring to reflect the actual guarantees.

Copilot uses AI. Check for mistakes.
Comment on lines +39 to +40
if self.duration < 0:
msg = f"duration must be a non-negative integer >= 0, got {self.duration}"
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_SportsValues currently allows duration=0, but later computations divide by sport.duration when computing thresholds. This can lead to a ZeroDivisionError for user-supplied sports; consider requiring duration > 0 (and validating it as a positive int).

Suggested change
if self.duration < 0:
msg = f"duration must be a non-negative integer >= 0, got {self.duration}"
if self.duration <= 0:
msg = (
f"duration must be a positive integer > 0, got {self.duration}"
)

Copilot uses AI. Check for mistakes.
Comment on lines +259 to +262
sl = phs(
tdb=x,
tr=tr,
v=vr,
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_SportsValues.vr is part of the sport definition and is documented as a sport-specific relative air speed, but the model uses only the function input vr when calling phs (so sport.vr is unused). This is confusing for API users; either incorporate sport.vr into the air-speed used by the model (e.g., combine ambient and self-generated speed) or remove vr from _SportsValues/docs.

Suggested change
sl = phs(
tdb=x,
tr=tr,
v=vr,
# Combine ambient air speed with sport-specific relative air speed
v_effective = vr + sport.vr
sl = phs(
tdb=x,
tr=tr,
v=v_effective,

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +118
assert 0 <= result_running.risk_level_interpolated <= 3
assert 0 <= result_walking.risk_level_interpolated <= 3
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These chained comparisons assume risk_level_interpolated is a Python float, but for scalar inputs the implementation returns a 0-d np.ndarray (from np.vectorize), which can raise "truth value of an array is ambiguous". Convert to a scalar first (e.g., float(np.asarray(...).item())) or use np.all for array-safe comparisons.

Suggested change
assert 0 <= result_running.risk_level_interpolated <= 3
assert 0 <= result_walking.risk_level_interpolated <= 3
assert 0 <= float(result_running.risk_level_interpolated) <= 3
assert 0 <= float(result_walking.risk_level_interpolated) <= 3

Copilot uses AI. Check for mistakes.
Comment on lines +229 to +230
assert 2.0 <= result_high.risk_level_interpolated < 3.0
assert "Apply active cooling strategies" == str(result_high.recommendation)
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

risk_level_interpolated may be a 0-d NumPy array for scalar inputs; chained comparisons like 2.0 <= ... < 3.0 can fail with an ambiguous truth-value error. Convert to a scalar first (e.g., via .item()) before comparing.

Copilot uses AI. Check for mistakes.

risk_level_interpolated = np.nan
# calculate the risk level with one decimal place
if min_t_medium <= tdb < t_medium:
Copy link

Copilot AI Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test is always true, because of this condition.

Suggested change
if min_t_medium <= tdb < t_medium:
if tdb < t_medium:

Copilot uses AI. Check for mistakes.
@FedericoTartarini FedericoTartarini merged commit 22d84c4 into development Feb 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants