model_sports_heat_stress#281
Conversation
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.
…ass and update risk calculation logic
build: changed tox docs command for paths with a space
…tress risk result
…d parameter descriptions and examples
…ress risk function
|
@yehui-h there is an issue with the code risk_level_interpolated : 2.00 but the recommendation is wrong. Please also check the remaining open comments from Coderabbit |
There was a problem hiding this comment.
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 | 🟠 MajorGuard the dew‑point log domain for 0% RH
With
rh == 0,e_sbecomes 0 and thenp.logcalls return-inf, so the function yields non‑finite outputs for an input that currently passes validation. Either rejectrh <= 0or explicitly handle it (e.g., returnnp.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 | 🟠 MajorAdd type validation for
tdbandrhviavalidate_typeThis aligns with repo patterns and ensures a clear
TypeErrorfor 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.
| 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" | ||
| ) |
There was a problem hiding this comment.
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,))🤖 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.
| # 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 | ||
| ) |
There was a problem hiding this comment.
🧹 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.
| 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 |
There was a problem hiding this comment.
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.
| """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 |
There was a problem hiding this comment.
🧹 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.
| 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) | ||
|
|
There was a problem hiding this comment.
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.
…ity with Python 3.10+
There was a problem hiding this comment.
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
| pytest = "*" | ||
| filelock = "*" | ||
| requests = "*" |
There was a problem hiding this comment.
🧩 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:
pytestthrough 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 pointtempfile/pytest at it via environment variables (incl.PYTEST_DEBUG_TEMPROOT), and consider enabling Linuxfs.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
SoftFileLockand 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
filelockreleases 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):
.netrccredential leak via maliciously crafted URLs — affectsrequests< 2.32.4; fixed in 2.32.4. Workaround for older versions: disable.netrcusage withtrust_env=Falseon the Session. [1] - Linux vendor advisories in 2025 (example): SUSE shipped an update on June 18, 2025 addressing CVE-2024-47081 in their
python-requestspackage. [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.
…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 …
There was a problem hiding this comment.
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 | 🟡 MinorBump
numpylower bound to avoid versions incompatible with Python 3.10.
numpy>=1.21allows 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 leastnumpy>=1.21.3(which introduced official Python 3.10 support). Alternatively,numpy>=1.22is 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.
| if self.duration < 0: | ||
| msg = f"duration must be a non-negative integer >= 0, got {self.duration}" | ||
| raise ValueError(msg) |
There was a problem hiding this comment.
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.
| 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)) |
There was a problem hiding this comment.
🧹 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.
| 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" |
There was a problem hiding this comment.
🧹 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.
| # 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) |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 aSportsnamespace 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. |
| 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] |
There was a problem hiding this comment.
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.
| 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 |
| """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 |
There was a problem hiding this comment.
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.
| """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, | |
| ) |
| assert 1.0 <= result_medium.risk_level_interpolated < 2.0 | ||
| assert "Increase frequency and/or duration of rest breaks" == str( |
There was a problem hiding this comment.
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.
| The equation use the Magnus formula using the coefficients from | ||
| the 2024 edition of the Guide to Instruments and Methods of | ||
| Observation. [WMO2024]_. |
There was a problem hiding this comment.
Grammar: "The equation use" should be "The equation uses".
| 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]_. |
| 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. | ||
| """ |
There was a problem hiding this comment.
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.
| if self.duration < 0: | ||
| msg = f"duration must be a non-negative integer >= 0, got {self.duration}" |
There was a problem hiding this comment.
_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).
| 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}" | |
| ) |
| sl = phs( | ||
| tdb=x, | ||
| tr=tr, | ||
| v=vr, |
There was a problem hiding this comment.
_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.
| 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, |
| assert 0 <= result_running.risk_level_interpolated <= 3 | ||
| assert 0 <= result_walking.risk_level_interpolated <= 3 |
There was a problem hiding this comment.
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.
| 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 |
| assert 2.0 <= result_high.risk_level_interpolated < 3.0 | ||
| assert "Apply active cooling strategies" == str(result_high.recommendation) |
There was a problem hiding this comment.
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.
|
|
||
| risk_level_interpolated = np.nan | ||
| # calculate the risk level with one decimal place | ||
| if min_t_medium <= tdb < t_medium: |
There was a problem hiding this comment.
Test is always true, because of this condition.
| if min_t_medium <= tdb < t_medium: | |
| if tdb < t_medium: |
fix: format heat_index_lu.py
…s heat stress risk
…t stress calculation
fix: adjust risk level calculation to truncate instead of round
Description
Add the Sports Heat Stress Risk model
Type of change
Please delete options that are not relevant.
Changes to Core Features:
New Feature Submissions:
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests
Chores