Adding a Thermal Comfort Function#
Use this guide when contributing a new thermal comfort model or utility
function to pythermalcomfort.
Quick checklist#
Before opening a PR, confirm:
[ ] Implementation added under
pythermalcomfort/models/<module_name>.py[ ] Input dataclass created/updated with validation in
__post_init__[ ] Return dataclass added/updated in
classes_return.py[ ] NumPy-style docstring with units, examples, and applicability limits
[ ] Tests added: scalars, arrays, broadcasting, invalid inputs
[ ]
autofunctionentry added in the relevantdocs/RST file[ ] CHANGELOG and AUTHORS updated (if applicable)
[ ] All tests pass; formatting and linting applied
Step-by-step guide#
1. Pick the module location#
Domain model →
pythermalcomfort/models/<module_name>.pyGeneric utility →
pythermalcomfort/utilities.py
2. Implement the function#
Keep it small, pure, and documented. Use NumPy for numeric operations. Follow this skeleton:
# pythermalcomfort/models/my_model.py
import numpy as np
from pythermalcomfort.classes_input import MyModelInputs
from pythermalcomfort.classes_return import MyModelResult
def my_model(
tdb: float | list[float],
v: float | list[float],
units: str = "SI",
limit_inputs: bool = True,
) -> MyModelResult:
"""One-line summary.
Parameters
----------
tdb : float or list of float
Dry-bulb air temperature, [°C] in [°F] if ``units`` = 'IP'.
v : float or list of float
Air speed, [m/s].
units : {'SI', 'IP'}, optional
Unit system. Defaults to 'SI'.
limit_inputs : bool, optional
If ``True``, returns NaN for inputs outside applicability limits.
Returns
-------
MyModelResult
Dataclass with fields ``result`` and ``valid``.
Examples
--------
.. code-block:: python
from pythermalcomfort.models import my_model
out = my_model(tdb=25, v=0.1)
print(out.result) # 42.0
"""
MyModelInputs(tdb=tdb, v=v, units=units)
tdb = np.asarray(tdb, dtype=float)
v = np.asarray(v, dtype=float)
result = ... # computation
return MyModelResult(result=result)
3. Create / update the input dataclass#
Add input dataclasses to pythermalcomfort/classes_input.py.
Put type checks and physical/applicability checks in __post_init__.
@dataclass
class MyModelInputs(BaseInputs):
tdb: float | int | list | np.ndarray = None
v: float | int | list | np.ndarray = None
def __post_init__(self):
super().__post_init__()
validate_type(self.tdb, "tdb", (float, int, list, np.ndarray))
validate_type(self.v, "v", (float, int, list, np.ndarray))
if np.any(np.asarray(self.v) < 0):
raise ValueError("v must be non-negative")
4. Create / update the return dataclass#
Add output dataclasses to pythermalcomfort/classes_return.py.
Inherit from AutoStrMixin for a formatted __str__ and __getitem__
dict-style access.
@dataclass
class MyModelResult(AutoStrMixin):
result: float | np.ndarray
5. Export the function#
Add the function to pythermalcomfort/models/__init__.py so it is
accessible as from pythermalcomfort.models import my_model.
6. Write tests#
Add tests under tests/test_my_model.py. Cover:
Scalar inputs (single values).
Vectorised inputs (lists and NumPy arrays).
Broadcasting behaviour and consistent output shapes.
Invalid inputs (
TypeErrorandValueErrorcases).Edge cases (zeros, very small/large inputs).
import numpy as np
import pytest
from pythermalcomfort.models import my_model
def test_scalar():
out = my_model(tdb=25, v=0.1)
assert out.result == pytest.approx(42.0, abs=0.1)
def test_array():
out = my_model(tdb=[25, 26], v=0.1)
assert len(out.result) == 2
def test_invalid_v():
with pytest.raises(ValueError):
my_model(tdb=25, v=-1.0)
7. Documentation#
Add a short executable example to the function docstring (scalar and array).
Add an
.. autofunction::entry in the relevantdocs/documentation/RST file (e.g.docs/documentation/models.rst).For larger tutorials, add a notebook under
docs/documentation/.
9. Final checks and PR#
ruff format ./pythermalcomfort ./tests
ruff check --fix ./pythermalcomfort ./tests
docformatter -r -i --wrap-summaries 88 --wrap-descriptions 88 pythermalcomfort
pytest tests/
PR description should include:
What the function computes and why it is useful.
Applicability limits and physical constraints.
How it was tested and any notes on numeric stability.
Validation rules common to most functions#
Non-negativity — validate physical quantities that must be ≥ 0.
Domain checks — avoid logs/roots of non-positive numbers.
Shape/broadcasting — verify array shapes are compatible when multiple array inputs are accepted.
Units — document expected units; convert via
units_converterwhenunits='IP'is supported.Error types — use
TypeErrorfor wrong types,ValueErrorfor invalid values.
Reference implementations#
Use these existing files as style references:
pythermalcomfort/models/pmv_ppd_iso.py— full model with input/output dataclasses, limit_inputs, unit conversion.pythermalcomfort/models/adaptive_ashrae.py— model that exposes module-level constants (SLOPE,INTERCEPT) for use by other modules.pythermalcomfort/utilities.py— shared helpers; add utility functions here when they are needed by more than one model.