1R1C
Overview
1R1C thermal RC model (single resistance, single capacitance).
Purpose and scope Computes indoor air temperature and heating/cooling load required to keep setpoints using a minimal grey-box model with one thermal resistance (R) to ambient and one thermal capacitance (C) representing the zone’s thermal inertia. Ventilation losses and internal/solar gains can be accounted for via auxiliary strategies or direct inputs.
Model sketch (discrete time)
T_in[t+1] = T_in[t] + (Δt/C) · ( (T_out[t] − T_in[t])/R
+ G_int[t] + G_sol[t] + H_ve[t]·(T_out[t] − T_in[t])
+ P_heat[t] − P_cool[t] ). P_heat / P_cool are clipped to
maximum device powers and only active when setpoint violations
would occur.
Typical use Quick load estimates, large batch simulations, or control studies where a lightweight model is sufficient and detailed envelope dynamics are not required. For more detailed dynamics, consider 5R1C (ISO 13790) or 7R2C (VDI 6007).
References Grey-box RC modeling of buildings (overview): many texts incl. ISO 13790 annexes; see also VDI 6007 for extended multi-node approaches.
Key facts
Method key:
1R1CSupported types:
hvac
Requirements
Required keys (specify in objects)
capacitance[J K-1]resistance[K W-1]weather
Optional keys (specify in objects)
power_heating[W]power_cooling[W]active_heatingactive_coolingactive_gains_internalactive_gains_solaractive_ventilationinit_temperature[C]min_temperature[C]max_temperature[C]deadband[K]target_humidity_rel[1]gains_internal_latent[W]area[m2]height[m]gains_internal[W]gains_internal_columnventilation[W K-1]ventilation_columngains_solar[W]windows
Required data (specify in data)
weather
Optional data (specify in data)
windowsgains_internal[W]gains_solar[W]ventilation[W K-1]
Outputs
Summary metrics
Key |
Description |
|---|---|
|
total heating demand |
|
maximum heating load |
|
total cooling demand (sensible + latent) |
|
maximum cooling load (sensible + latent) |
Timeseries columns
Column |
Description |
|---|---|
|
indoor air temperature |
|
heating load |
|
total cooling load (sensible + latent) |
|
sensible cooling load |
|
latent cooling load |
Public methods
generate
def generate( self, obj: dict = None, data: dict = None, results: dict | None = None, ts_type: str = Types.HVAC, *, capacitance: float = None, resistance: float = None, weather: pd.DataFrame = None, power_heating: float | pd.Series = None, power_cooling: float | pd.Series = None, active_heating: bool = None, active_cooling: bool = None, ventilation: float = None, temp_init: float = None, temp_min: float = None, temp_max: float = None, gains_internal: pd.DataFrame = None, gains_solar: pd.DataFrame = None, area: float = None, height: float = None, ): """Generate HVAC time series based on input parameters and weather data. This method implements the abstract generate method from the Method base class. It processes the input parameters, calculates the indoor temperature and energy demand time series, and returns both the time series and summary statistics. Args: obj (dict, optional): Dictionary containing building parameters. data (dict, optional): Dictionary containing input data. results (dict, optional): Dictionary with results from previously generated time series ts_type (str, optional): Time series type to generate. Defaults to Types.HVAC. capacitance (float, optional): Thermal capacitance in J/K. resistance (float, optional): Thermal resistance in K/W. weather (pd.DataFrame, optional): Weather data with outdoor temperature. power_heating (float or pd.Series, optional): Maximum heating power in W. power_cooling (float or pd.Series, optional): Maximum cooling power in W. active_heating (bool, optional): Whether heating is active. active_cooling (bool, optional): Whether cooling is active. ventilation (float, optional): Ventilation rate in W/K. temp_init (float, optional): Initial indoor temperature in °C. temp_min (float, optional): Minimum indoor temperature in °C. temp_max (float, optional): Maximum indoor temperature in °C. gains_internal (pd.DataFrame, optional): Internal SENSIBLE heat gains in W. Do NOT include the latent portion here — pass that via `gains_internal_latent[W]` on the object. ASHRAE metabolic tables split a seated occupant as ~75 W sensible + ~55 W latent; the sensible number is what belongs in this input. gains_solar (pd.DataFrame, optional): Solar heat gains in W. area (float, optional): Heated area in m². height (float, optional): Heated height in m³. Returns: dict: Dictionary containing: - "summary" (dict): Summary statistics including total heating and cooling demand, and maximum heating and cooling loads. - "timeseries" (pd.DataFrame): Time series of indoor temperature, heating load, and cooling load with timestamps as index. Raises: Exception: If required data is missing or invalid. """ obj, data = self._process_kwargs( obj, data, capacitance=capacitance, resistance=resistance, weather=weather, power_heating=power_heating, power_cooling=power_cooling, active_heating=active_heating, active_cooling=active_cooling, ventilation=ventilation, temp_init=temp_init, temp_min=temp_min, temp_max=temp_max, gains_internal=gains_internal, gains_solar=gains_solar, area=area, height=height, ) obj, data = self._get_input_data(obj, data, ts_type) # Timestep in seconds (assuming a regular time grid). Compute from # just the first two timestamps — casting the whole column via # `.values.astype("datetime64[ns]")` costs ~20 ms/object on a # timezone-aware DatetimeIndex because pandas materializes the object # dtype first. Only the delta matters. dt_col = data[O.WEATHER][C.DATETIME] timestep = np.float32((dt_col.iloc[1] - dt_col.iloc[0]).total_seconds()) # Precompute auxiliary data data[O.GAINS_INTERNAL] = InternalGains().generate(obj, data) data[O.GAINS_SOLAR] = SolarGains().generate(obj, data) data[O.VENTILATION] = Ventilation().generate(obj, data) # Compute temperature and energy demand temp_in, p_heat, p_cool = calculate_timeseries_1r1c(obj, data, timestep) # Latent-cooling post-pass (issue #103). Decoupled from the T_in # dynamics — 1R1C carries no humidity state — so this runs vectorised # after the sensible solver, leaving the numba path untouched. # `p_cool` returned by the solver is already sensible-clipped to # `power_cooling` (the total cap); the post-pass may clip it further # if latent load pushes the total over that cap. weather_df = data[O.WEATHER] p_cool_max_arr = resolve_ts_or_scalar( obj, data, O.POWER_COOLING, weather_df.index, default=DEFAULT_POWER_COOLING ).to_numpy(dtype=np.float32, copy=False) h_ve_arr = data[O.VENTILATION].to_numpy(dtype=np.float32, copy=False).ravel() gains_lat_arr = resolve_ts_or_scalar( obj, data, O.GAINS_INTERNAL_LATENT, weather_df.index, default=DEFAULT_GAINS_INTERNAL_LATENT ).to_numpy(dtype=np.float32, copy=False) active_cool = bool(obj.get(O.ACTIVE_COOLING, DEFAULT_ACTIVE_COOLING)) if active_cool: p_cool_sensible, p_cool_latent = compute_latent_cooling( weather=weather_df, p_cool_sensible=p_cool, p_cool_max=p_cool_max_arr, h_ve=h_ve_arr, gains_internal_latent=gains_lat_arr, temp_max_c=float(obj.get(O.TEMP_MAX, DEFAULT_TEMP_MAX)), target_humidity_rel=float(obj.get(O.TARGET_HUMIDITY_REL, DEFAULT_TARGET_HUMIDITY_REL)), ) else: p_cool_sensible = p_cool p_cool_latent = np.zeros_like(p_cool) logger.debug( f"[HVAC R1C1] {ts_type}: max heating {p_heat.max()}, " f"cooling sensible {p_cool_sensible.max()}, latent {p_cool_latent.max()}" ) return self._format_output(temp_in, p_heat, p_cool_sensible, p_cool_latent, data, timestep)