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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ochre/Models/Envelope.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ def update_infiltration(
run_nat_vent = (w_amb < max_oa_hr) and (t_zone > t_ext) and (t_zone > t_base)
if run_nat_vent and self.open_window_area is not None:
area = self.open_window_area * 0.6
nat_vent_area = convert(area, "ft^2", "cm^2")
nat_vent_area = convert(area, "m^2", "cm^2")
max_nat_flow = convert(20 * self.volume, "m^3/hr", "m^3/s") # max 20 ACH
adj = (t_zone - t_base) / (t_zone - t_ext)
adj = max(min(adj, 1), 0)
Expand Down
69 changes: 69 additions & 0 deletions test/test_models/test_envelope.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import unittest
import datetime as dt
from unittest.mock import patch
import numpy as np
import pandas as pd

from ochre.Models.Envelope import Envelope, ExteriorZone
from ochre.utils import OCHREException
from ochre.utils.units import convert


# Common simulation parameters
Expand Down Expand Up @@ -222,6 +224,73 @@ def test_linearize_infiltration(self):
self.assertTrue(envelope.linearize_infiltration)


class NaturalVentilationUnitTestCase(unittest.TestCase):
"""Tests for natural ventilation unit consistency.

open_window_area is computed in m^2 (from HPXML import which converts ft^2 → m^2).
The natural ventilation calculation must treat it as m^2 throughout.
"""

def _make_zone_with_nat_vent(self, open_window_area_m2: float = 1.0) -> "Zone":
"""Create a minimal Indoor zone with natural ventilation enabled."""
envelope = create_minimal_envelope(
zones={
"Indoor": {
"Volume (m^3)": 600,
# ELA coefficients are needed for nat vent flow formula
"ELA stack coefficient (L/s/cm^4/K)": 0.00029,
"ELA wind coefficient (L/s/cm^4/(m/s))": 0.000231,
"enable_humidity": False,
},
},
)
zone = envelope.indoor_zone
zone.open_window_area = open_window_area_m2
return zone

def test_nat_vent_area_uses_m2_not_ft2(self):
"""Natural ventilation should convert open window area from m^2 to cm^2."""
vent_kwargs = dict(t_ext=20.0, t_zone=25.0, wind_speed=2.0, density=1.2, w_amb=0.005)
zone = self._make_zone_with_nat_vent(open_window_area_m2=1.0)

with patch("ochre.Models.Envelope.convert", wraps=convert) as convert_mock:
zone.update_infiltration(**vent_kwargs)

area_conversion_calls = [call for call in convert_mock.call_args_list if call.args[2] == "cm^2"]

self.assertGreater(zone.nat_vent_flow, 0, "Natural ventilation should be active")
self.assertEqual(len(area_conversion_calls), 1)
self.assertEqual(area_conversion_calls[0].args[1:], ("m^2", "cm^2"))

def test_nat_vent_not_triggered_when_conditions_unmet(self):
"""Natural ventilation should be zero when conditions are not met."""
zone = self._make_zone_with_nat_vent(open_window_area_m2=1.0)

# t_zone < t_ext → should NOT trigger nat vent
zone.update_infiltration(
t_ext=30.0,
t_zone=20.0,
wind_speed=2.0,
density=1.2,
w_amb=0.005,
)
self.assertEqual(zone.nat_vent_flow, 0)

def test_nat_vent_zero_when_no_window_area(self):
"""Natural ventilation should be zero when open_window_area is None."""
zone = self._make_zone_with_nat_vent()
zone.open_window_area = None

zone.update_infiltration(
t_ext=20.0,
t_zone=25.0,
wind_speed=2.0,
density=1.2,
w_amb=0.005,
)
self.assertEqual(zone.nat_vent_flow, 0)


class EnvelopeRadiationTestCase(unittest.TestCase):
"""Tests for radiation methods"""

Expand Down