Skip to content

Sports Heat Stress Risk equation#286

Merged
FedericoTartarini merged 64 commits intomasterfrom
development
Feb 17, 2026
Merged

Sports Heat Stress Risk equation#286
FedericoTartarini merged 64 commits intomasterfrom
development

Conversation

@FedericoTartarini
Copy link
Collaborator

@FedericoTartarini FedericoTartarini commented Feb 17, 2026

Summary by CodeRabbit

  • New Features

    • Added sports heat stress risk assessment to evaluate thermal strain for athletes during outdoor activities based on environmental conditions.
  • Bug Fixes

    • Updated dew point calculation formula to WMO 2024 standard with enhanced input validation.
    • Applied security updates to critical dependencies.
  • Documentation

    • Added sports heat stress risk guidance and recommendations.
    • Enhanced survey documentation with thermal disposition options.
  • Chores

    • Version bumped to 3.9.0.

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 and others added 25 commits February 3, 2026 16:48
…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 …
fix: adjust risk level calculation to truncate instead of round
model_sports_heat_stress
Copilot AI review requested due to automatic review settings February 17, 2026 07:27
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

Caution

Review failed

The pull request is closed.

Walkthrough

This PR introduces a version 3.9.0 release featuring a new sports heat stress risk assessment model. It includes a comprehensive sports heat stress risk calculation function with predefined sport profiles, updated input validation classes, new return dataclasses, enhanced dew point utilities aligned with WMO2024 standards, and corresponding documentation and test coverage.

Changes

Cohort / File(s) Summary
Version Metadata
.bumpversion.toml, pythermalcomfort/__init__.py, docs/conf.py, setup.py
Updated version from 3.8.0 to 3.9.0 across configuration files and Python metadata; added type annotation to __version__.
Sports Heat Stress Risk Model
pythermalcomfort/models/sports_heat_stress_risk.py, pythermalcomfort/models/__init__.py
Introduced new sports_heat_stress_risk() function and Sports class for heat stress risk assessment; exports added to module public API.
Input/Output Classes
pythermalcomfort/classes_input.py, pythermalcomfort/classes_return.py
Added SportsHeatStressInputs class with validation logic for sport type and environmental parameters; added SportsHeatStressRisk frozen dataclass for results.
Documentation
docs/documentation/models.rst, docs/documentation/references.rst, docs/documentation/surveys.rst
Documented new sports heat stress risk model with autofunction and autoclass directives; added references (SportsHeatStress2025, SportsHeatPolicy2025, SportsHeatTool); added Thermal Disposition subsection to survey documentation.
Testing
tests/test_sports_heat_stress_risk.py
Added comprehensive test suite covering scalar/array inputs, broadcasting, validation, recommendations, threshold capping, edge cases, and monotonicity checks.
Utilities & Dependencies
pythermalcomfort/utilities.py, Pipfile
Enhanced dew_point_tmp() with WMO2024 coefficients and RH validation; updated Pipfile with CVE-related version constraints (filelock >=3.20.1, requests >=2.32.4, pytest).
Supporting Metadata
AUTHORS.rst, CHANGELOG.rst, README.rst, pythermalcomfort/models/heat_index_lu.py
Added contributor Yehui Huang; updated CHANGELOG for version 3.9.0; updated security badge to shields.io; reformatted lambda expressions in heat_index_lu for readability.

Sequence Diagram

sequenceDiagram
    participant User as User/Client
    participant API as sports_heat_stress_risk()
    participant Validator as SportsHeatStressInputs
    participant Sport as Sports.RUNNING<br/>(or other)
    participant Calc as _calc_risk_single_value()
    participant Return as SportsHeatStressRisk

    User->>API: Call sports_heat_stress_risk(tdb, tr, rh, vr, sport)
    API->>Validator: Validate inputs (type, range, broadcasting)
    Validator-->>API: Validated & array-converted inputs
    
    API->>Calc: Vectorize _calc_risk_single_value<br/>over input arrays
    
    loop For each input combination
        Calc->>Sport: Fetch sport thresholds<br/>(t_medium, t_high, t_extreme)
        Sport-->>Calc: Sport parameters
        Calc->>Calc: Root-find thresholds via brentq<br/>(sweat loss, core temp equations)
        Calc->>Calc: Interpolate risk_level across bands
        Calc->>Calc: Generate recommendation<br/>via _get_recommendation()
    end
    
    Calc-->>API: Per-element risk results
    API->>Return: Create SportsHeatStressRisk dataclass
    Return-->>User: Return aggregated results<br/>(risk_level, t_medium, t_high, t_extreme, recommendation)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #275: Modifies utilities.py dew_point_tmp function with WMO2024 coefficients and validation—directly impacts shared utility functions updated in this release.
  • PR #281: Introduces the same sports_heat_stress_risk feature with Sports class and risk assessment logic—substantially overlapping feature implementation.
  • PR #280: Adds sports heat stress risk model with supporting input/return dataclasses and documentation—parallel feature addition at code level.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch development

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@FedericoTartarini FedericoTartarini merged commit df928fb into master Feb 17, 2026
7 of 8 checks passed
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, including inputs/outputs, documentation, and a comprehensive test suite, along with a version bump to 3.9.0.

Changes:

  • Introduces sports_heat_stress_risk() and a Sports namespace of predefined sport parameters.
  • Adds SportsHeatStressInputs (input validation) and SportsHeatStressRisk (return dataclass).
  • Updates docs/references and bumps package version/dependency constraints; adds a large new test module.

Reviewed changes

Copilot reviewed 18 out of 19 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pythermalcomfort/models/sports_heat_stress_risk.py New model implementation and sport parameter definitions.
pythermalcomfort/classes_input.py Adds SportsHeatStressInputs with broadcasting + range validation.
pythermalcomfort/classes_return.py Adds SportsHeatStressRisk return dataclass.
pythermalcomfort/models/__init__.py Re-exports Sports and sports_heat_stress_risk.
tests/test_sports_heat_stress_risk.py Adds extensive tests for scalar/array/broadcasting/edge cases.
docs/documentation/models.rst Adds API docs entries for the new model.
docs/documentation/references.rst Adds citations used by the new model docs.
setup.py Bumps version and pins NumPy range.
pythermalcomfort/__init__.py Bumps __version__ and adds an explicit type annotation.
CHANGELOG.rst Adds 3.9.0 entry describing the new model.
pythermalcomfort/utilities.py Docstring edits for dew_point_tmp.
pythermalcomfort/models/heat_index_lu.py Pure formatting change to lambdas for readability.
docs/documentation/surveys.rst Adds a new “Thermal Disposition” survey section.
README.rst Updates the Snyk badge URL/target.
Pipfile Adjusts dev dependency entries and adds CVE-related pins/comments.
.bumpversion.toml Updates current_version to 3.9.0.
AUTHORS.rst Adds a contributor entry.

Comment on lines +185 to +189
# 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]
)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

sports_heat_stress_risk() creates a new np.vectorize wrapper on every call. Since this function is computationally heavy (root finding + PHS), consider defining the vectorized wrapper once at module scope (or using the @np.vectorize decorator) to avoid repeated wrapper construction overhead.

Copilot uses AI. Check for mistakes.
Comment on lines +706 to +730
"""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 17, 2026

Choose a reason for hiding this comment

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

test_sports_heat_stress_risk_inf_values uses try/except ...: pass, which can make the test pass without asserting any behavior (and Warning isn’t normally raised as an exception). Prefer asserting one expected outcome (either a specific exception via pytest.raises or a valid result with explicit checks), and use pytest.warns if warnings are expected.

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 considered invalid input
with pytest.raises(ValueError):
sports_heat_stress_risk(
tdb=np.inf,
tr=30,
rh=50,
vr=0.5,
sport=Sports.RUNNING,
)
# Negative infinity should also be considered invalid input
with pytest.raises(ValueError):
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 +320 to +365
def test_sports_heat_stress_risk_all_sports():
"""Test that all predefined sports in Sports dataclass work correctly."""
# Get all sport attributes from Sports dataclass
all_sports = [
Sports.ABSEILING,
Sports.ARCHERY,
Sports.AUSTRALIAN_FOOTBALL,
Sports.BASEBALL,
Sports.BASKETBALL,
Sports.BOWLS,
Sports.CANOEING,
Sports.CRICKET,
Sports.CYCLING,
Sports.EQUESTRIAN,
Sports.FIELD_ATHLETICS,
Sports.FIELD_HOCKEY,
Sports.FISHING,
Sports.GOLF,
Sports.HORSEBACK,
Sports.KAYAKING,
Sports.RUNNING,
Sports.MTB,
Sports.NETBALL,
Sports.OZTAG,
Sports.PICKLEBALL,
Sports.CLIMBING,
Sports.ROWING,
Sports.RUGBY_LEAGUE,
Sports.RUGBY_UNION,
Sports.SAILING,
Sports.SHOOTING,
Sports.SOCCER,
Sports.SOFTBALL,
Sports.TENNIS,
Sports.TOUCH,
Sports.VOLLEYBALL,
Sports.WALKING,
]

# Test moderate conditions with each sport
tdb, tr, rh, vr = 32, 32, 50, 0.5

for sport in all_sports:
result = sports_heat_stress_risk(tdb=tdb, tr=tr, rh=rh, vr=vr, sport=sport)

# Verify all sports return valid results
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

This test loops through every sport and calls sports_heat_stress_risk each time. Because the model performs root-finding and repeatedly calls phs, this is likely to be very slow/flaky for unit tests. Consider reducing coverage to a representative subset here, or marking this test as slow and moving comprehensive coverage to a separate slow test suite.

Copilot uses AI. Check for mistakes.
Comment on lines +550 to +561
def test_sports_heat_stress_risk_large_array():
"""Test with large arrays (performance and correctness)."""
# Create large arrays
size = 100
tdb_array = np.linspace(10, 45, size)
tr_array = np.linspace(10, 45, size)
rh_array = np.full(size, 50)
vr_array = np.full(size, 0.5)

result = sports_heat_stress_risk(
tdb=tdb_array, tr=tr_array, rh=rh_array, vr=vr_array, sport=Sports.RUNNING
)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

test_sports_heat_stress_risk_large_array uses a length-100 array; with per-element root finding and PHS evaluation this can be very expensive in CI. Consider reducing size to a small number sufficient to validate vectorization/broadcasting, or marking the test as slow/performance.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

In the docstring, “The equation use …” is grammatically incorrect. Consider changing it to “The equation uses …” for clarity.

Suggested change
The equation use the Magnus formula using the coefficients from
The equation uses the Magnus formula using the coefficients from

Copilot uses AI. Check for mistakes.
Comment on lines +48 to +50
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 17, 2026

Choose a reason for hiding this comment

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

The Sports docstring claims that using a frozen dataclass prevents modification of the namespace, but frozen=True only affects instances and does not prevent reassignment of class attributes (and _SportsValues instances themselves are mutable). Either adjust the docstring to match actual behavior or make the stored values immutable (e.g., make _SportsValues frozen and avoid implying the namespace can’t be modified).

Suggested change
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.
Use attributes like ``Sports.RUNNING`` to obtain a :class:`_SportsValues`
instance containing the corresponding clothing insulation (clo), metabolic
rate (met), air velocity (vr), and typical activity duration.
The attributes defined on this class are intended to be used as constants
for common sports, and are provided as class-level attributes rather than
instance fields. The ``frozen=True`` dataclass option applies to instances
of :class:`Sports` and does not prevent reassignment of these class
attributes or mutation of the stored :class:`_SportsValues` instances.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +23
clo: float
met: float
vr: float
duration: int

Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

_SportsValues defines a vr attribute and the public docs for sport mention it, but sport.vr is never used in the calculation (only the function argument vr is used). This is confusing for API consumers; either incorporate sport.vr into the model (e.g., as a default/override) or remove the field/documentation so there’s a single source of truth for air speed.

Copilot uses AI. Check for mistakes.
Comment on lines +227 to +231
sweat_loss_g = 850 # 850 g per hour

max_t_low = 34.5 # maximum tdb for low risk
max_t_medium = 39 # maximum tdb for medium risk
max_t_high = 43.5 # maximum tdb for high risk
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The sweat_loss_g threshold is commented as “per hour”, but later the root function scales modeled sweat loss to a 45-minute basis (multiplying by 45). Please clarify the intended units and ensure the threshold and scaling are consistent (e.g., convert the threshold to the same time basis or use 60 minutes if the threshold is hourly).

Copilot uses AI. Check for mistakes.
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