Ejemplo n.º 1
0
    def allocate_output_memory(
        self, t_start, t_end, t_step, data_spec, categories_dict
    ):
        """preallocate output memory as numpy arrays to speed up simulation"""
        # reset output memory
        self.output = {}
        self.fmu_output = {}

        self.output = {
            STATES.SIMULATION_TIME: np.arange(
                t_start, t_end + t_step, t_step, dtype="int64"
            )
        }
        n_s = len(self.output[STATES.SIMULATION_TIME])

        # add output state variables
        for state in self.output_states:
            if data_spec.full.spec[state]["dtype"] == "category":
                self.output[state] = pd.Series(
                    pd.Categorical(
                        pd.Series(index=np.arange(n_s)),
                        categories=categories_dict[state],
                    )
                )
            else:
                (
                    np_default_value,
                    np_dtype,
                ) = Conversions.numpy_down_cast_default_value_dtype(
                    data_spec.full.spec[state]["dtype"]
                )
                self.output[state] = np.full(
                    n_s,
                    np_default_value,
                    dtype=np_dtype,
                )

        # add fmu state variables
        self.fmu_output[STATES.STEP_STATUS] = np.full(n_s, False, dtype="bool")
        self.fmu_output[STATES.SIMULATION_TIME] = self.output[STATES.SIMULATION_TIME]

        for k, v in self.idf.output_spec.items():
            (
                np_default_value,
                np_dtype,
            ) = Conversions.numpy_down_cast_default_value_dtype(v["dtype"])
            self.fmu_output[k] = np.full(n_s, np_default_value, dtype=np_dtype)

        # set current time
        self.current_time = t_start
        self.current_t_idx = 0
    def update_output(self, status, step_sensor_input):
        """Update internal output obj for current_t_idx with fmu output."""
        self.fmu_output[STATES.STEP_STATUS][self.current_t_idx] = status

        # get fmi zone output
        for k, v in self.idf.output_spec.items():
            self.fmu_output[k][self.current_t_idx] = self.fmu.get(k)[0]

        # map fmu output to model output
        self.output[STATES.THERMOSTAT_TEMPERATURE][
            self.current_t_idx] = self.get_tstat_temperature()

        self.output[STATES.THERMOSTAT_HUMIDITY][
            self.current_t_idx] = Conversions.relative_humidity_from_dewpoint(
                temperature=self.output[STATES.THERMOSTAT_TEMPERATURE][
                    self.current_t_idx],
                dewpoint=self.get_tstat_dewpoint(),
            )

        # pass through motion
        self.output[STATES.THERMOSTAT_MOTION][
            self.current_t_idx] = step_sensor_input[STATES.THERMOSTAT_MOTION]

        # get step_output
        for state in self.output_states:
            self.step_output[state] = self.output[state][self.current_t_idx]
Ejemplo n.º 3
0
    def allocate_output_memory(self, t_start, t_end, t_step, data_spec,
                               categories_dict):
        """preallocate output memory to speed up simulation"""
        # reset output
        self.output = {}

        self.output = {
            STATES.SIMULATION_TIME:
            np.arange(t_start, t_end + t_step, t_step, dtype="int64")
        }
        n_s = len(self.output[STATES.SIMULATION_TIME])

        # add state variables
        for state in self.output_states:
            if data_spec.full.spec[state]["dtype"] == "category":
                self.output[state] = pd.Series(
                    pd.Categorical(
                        pd.Series(index=np.arange(n_s)),
                        categories=categories_dict[state],
                    ))
            else:
                (
                    np_default_value,
                    np_dtype,
                ) = Conversions.numpy_down_cast_default_value_dtype(
                    data_spec.full.spec[state]["dtype"])
                self.output[state] = np.full(
                    n_s,
                    np_default_value,
                    dtype=np_dtype,
                )

        self.output[STATES.STEP_STATUS] = np.full(n_s, 0, dtype="int8")
Ejemplo n.º 4
0
def spec_unit_conversion(df, src_spec, dest_spec):
    """This method must be able to evaluate multiple sources should
    a channel be composed from multiple sources."""
    for k, v in src_spec.full.spec.items():
        if k in df.columns:
            src_unit = v["unit"]
            # permutations on Internal spec usage
            dest_unit = None
            if isinstance(dest_spec, Internal):
                dest_unit = dest_spec.full.spec[v["internal_state"]]["unit"]
            elif isinstance(src_spec, Internal):
                for d_k, d_v in dest_spec.full.spec.items():
                    if d_v["internal_state"] == k:
                        dest_unit = d_v["unit"]
            else:
                for d_k, d_v in dest_spec.full.spec.items():
                    if d_v["internal_state"] == v["internal_state"]:
                        dest_unit = d_v["unit"]

            if dest_unit and src_unit != dest_unit:
                if (src_unit == UNITS.FARHENHEIT) and (dest_unit
                                                       == UNITS.CELSIUS):
                    df[k] = Conversions.F2C(df[k])
                elif (src_unit == UNITS.CELSIUS) and (dest_unit
                                                      == UNITS.FARHENHEIT):
                    df[k] = Conversions.C2F(df[k])
                elif (src_unit
                      == UNITS.FARHENHEITx10) and (dest_unit
                                                   == UNITS.FARHENHEIT):
                    df[k] = df[k] / 10.0
                elif (src_unit
                      == UNITS.FARHENHEIT) and (dest_unit
                                                == UNITS.FARHENHEITx10):
                    df[k] = df[k] * 10.0
                elif (src_unit == UNITS.FARHENHEITx10) and (dest_unit
                                                            == UNITS.CELSIUS):
                    df[k] = Conversions.F2C(df[k] / 10.0)
                elif (src_unit == UNITS.CELSIUS) and (dest_unit
                                                      == UNITS.FARHENHEITx10):
                    df[k] = Conversions.C2F(df[k]) * 10.0
                else:
                    logger.error("Unsupported conversion: {} to {}".format(
                        src_unit,
                        dest_unit,
                    ))
    return df
    def fill_epw(self, input_epw_data, datetime_channel, fill_epw_data,
                 sim_config):
        """Any missing fields required by EnergyPlus should be filled with
        defaults from Typical Meteorological Year 3 data sets for nearest city.
        All data is internally in UTC.

        :param epw_data: EnergyPlus Weather data in a dataframe of epw_columns
        :type epw_data: pd.DataFrame

        :param datetime_column: datetime column in fill data.
        "type datetime_column: str
        """
        if input_epw_data.empty:
            input_epw_data.columns = self.epw_columns
            return input_epw_data

        if fill_epw_data.empty:
            raise ValueError(f"fill_epw_data is empty.")

        # save fill_epw_data that was actually used to fill
        self.fill_epw_data = fill_epw_data

        # edit unique copy of input df
        epw_data = input_epw_data.copy(deep=True)

        # add datetime column for merge with fill data
        epw_data = pd.concat(
            [
                datetime_channel.data[datetime_channel.spec.datetime_column],
                epw_data,
            ],
            axis="columns",
        ).rename(columns={
            datetime_channel.spec.datetime_column: self.datetime_column
        })

        # get current period to check if resampling is needed
        _cur_fill_epw_data_period = (fill_epw_data[
            self.datetime_column].diff().mode()[0].total_seconds())

        if _cur_fill_epw_data_period < sim_config["sim_step_size_seconds"]:
            # downsample data
            fill_epw_data = (fill_epw_data.set_index(
                fill_epw_data[self.datetime_column]).resample(
                    f"{sim_config['sim_step_size_seconds']}S").mean().
                             reset_index())
        elif _cur_fill_epw_data_period > sim_config["sim_step_size_seconds"]:
            # upsample data
            fill_epw_data = fill_epw_data.set_index(self.datetime_column)
            fill_epw_data = fill_epw_data.resample(
                f"{sim_config['sim_step_size_seconds']}S").asfreq()
            # ffill is only method that works on all types
            fill_epw_data = fill_epw_data.interpolate(axis="rows",
                                                      method="ffill")
            fill_epw_data = fill_epw_data.reset_index()

        # using annual TMY there may be missing columns at beginning
        # cycle from endtime to give full UTC year
        # wrap TMY data to fill any gaps

        if min(fill_epw_data[self.datetime_column]) > min(
                epw_data[self.datetime_column]):
            # have data before fill data starts
            # wrap fill data on year
            time_diff = min(fill_epw_data[self.datetime_column]) - min(
                epw_data[self.datetime_column])
            years = math.ceil(time_diff.days / 365.0)
            fill_epw_data_prev_years = []
            for y in range(1, years):
                _fill_epw_data_prev_year = fill_epw_data.copy(deep=True)
                _fill_epw_data_prev_year[
                    "year"] = _fill_epw_data_prev_year["year"] - 1
                _fill_epw_data_prev_year[
                    self.datetime_column] = _fill_epw_data_prev_year[
                        self.datetime_column] - pd.offsets.DateOffset(years=1)
                fill_epw_data_prev_years.append(_fill_epw_data_prev_year)

            fill_epw_data = pd.concat(fill_epw_data_prev_years +
                                      [fill_epw_data],
                                      axis="rows")
            fill_epw_data.sort_values(self.datetime_column)

        if max(fill_epw_data[self.datetime_column]) < max(
                epw_data[self.datetime_column]):
            # have data before fill data starts
            # wrap fill data on year
            time_diff = max(epw_data[self.datetime_column]) - max(
                fill_epw_data[self.datetime_column])
            years = math.ceil(time_diff.days / 365.0)
            fill_epw_data_prev_years = []
            for y in range(1, years):
                _fill_epw_data_prev_year = fill_epw_data.copy(deep=True)
                _fill_epw_data_prev_year[
                    "year"] = _fill_epw_data_prev_year["year"] + 1
                _fill_epw_data_prev_year[
                    self.datetime_column] = _fill_epw_data_prev_year[
                        self.datetime_column] + pd.offsets.DateOffset(years=1)
                fill_epw_data_prev_years.append(_fill_epw_data_prev_year)

            fill_epw_data = pd.concat([fill_epw_data] +
                                      fill_epw_data_prev_years,
                                      axis="rows")
            fill_epw_data.sort_values(self.datetime_column)

        # epw_data left join fill_data will give fill data for every epw_data
        # record
        epw_data_full = epw_data[[self.datetime_column] +
                                 self.spec.columns].merge(
                                     fill_epw_data,
                                     how="left",
                                     on=[self.datetime_column],
                                 )

        # loop over spec columns and fill missing values
        # then set them to epw names
        for _col in self.spec.columns:
            epw_data_full.loc[epw_data_full[_col].isnull(),
                              _col, ] = epw_data_full[
                                  EnergyPlusWeather.output_rename_dict[_col]]
            epw_data_full[EnergyPlusWeather.
                          output_rename_dict[_col]] = epw_data_full[_col]
            epw_data_full = epw_data_full.drop(columns=[_col])

        # compute dewpoint from dry-bulb and relative humidity
        epw_data_full["temp_dew"] = Conversions.relative_humidity_to_dewpoint(
            epw_data_full["temp_air"], epw_data_full["relative_humidity"])

        # convert to local time INVARIANT to DST changes
        # .epw will have wrong hour columns if DST shift occurs during simulation
        # need a standard UTC offset for entire simulation period
        # no time zone shift occurs on or within 1 week of January 17th
        # use this for tz standard UTC offset
        tz_offset_seconds = datetime_channel.timezone.utcoffset(
            datetime(min(epw_data_full[self.datetime_column]).year, 1,
                     17)).total_seconds()

        epw_data_full[self.datetime_column] = epw_data_full[
            self.datetime_column] + pd.Timedelta(seconds=tz_offset_seconds)

        # last day of data must exist and be invariant to TZ shift
        # add ffill data for final day and extra day.
        _fill = epw_data_full.tail(1).copy(deep=True)
        _fill_rec = _fill.iloc[0]
        _fill[
            self.datetime_column] = _fill[self.datetime_column] + pd.Timedelta(
                days=2,
                hours=-_fill_rec[self.datetime_column].hour,
                minutes=-_fill_rec[self.datetime_column].minute,
                seconds=-_fill_rec[self.datetime_column].second,
            )
        epw_data_full = epw_data_full.append(_fill, ignore_index=True)
        epw_data_full = epw_data_full.set_index(self.datetime_column)
        epw_data_full = epw_data_full.resample(
            f"{sim_config['sim_step_size_seconds']}S").asfreq()
        # first ffill then bfill will fill both sides padding data
        epw_data_full = epw_data_full.fillna(method="ffill")
        epw_data_full = epw_data_full.fillna(method="bfill")
        epw_data_full = epw_data_full.reset_index()

        epw_data_full["year"] = epw_data_full[self.datetime_column].dt.year
        epw_data_full["month"] = epw_data_full[self.datetime_column].dt.month
        epw_data_full["day"] = epw_data_full[self.datetime_column].dt.day
        # energyplus uses non-standard hours [1-24] this is accounted in to_epw()
        epw_data_full["hour"] = epw_data_full[self.datetime_column].dt.hour
        epw_data_full["minute"] = epw_data_full[self.datetime_column].dt.minute

        # date time columns can be smaller dtypes
        epw_data_full = epw_data_full.astype(
            {
                "year": "Int16",
                "month": "Int8",
                "day": "Int8",
                "hour": "Int8",
                "minute": "Int8",
            }, )

        # reorder return columns
        return epw_data_full
    def generate_dummy_data(
        sim_config,
        spec,
        outdoor_weather=None,
        schedule_chg_pts=None,
        comfort_chg_pts=None,
        hvac_mode_chg_pts=None,
    ):
        if isinstance(spec, Internal):
            raise ValueError(
                f"Supplied Spec {spec} is internal spec." +
                " Data of this spec should not be stored in data files")

        for _idx, sim in sim_config.iterrows():
            # _df = pd.DataFrame(columns=spec.full.spec.keys())
            _df = pd.DataFrame(index=pd.date_range(
                start=sim.start_utc,
                end=sim.end_utc,
                freq=f"{spec.data_period_seconds}S",
            ))

            if not schedule_chg_pts:
                # set default ecobee schedule
                schedule_chg_pts = {
                    sim.start_utc: [
                        {
                            "name":
                            "Home",
                            "minute_of_day":
                            390,
                            "on_day_of_week": [
                                True,
                                True,
                                True,
                                True,
                                True,
                                True,
                                True,
                            ],
                        },
                        {
                            "name":
                            "Sleep",
                            "minute_of_day":
                            1410,
                            "on_day_of_week": [
                                True,
                                True,
                                True,
                                True,
                                True,
                                True,
                                True,
                            ],
                        },
                    ]
                }

            if not comfort_chg_pts:
                # set default ecobee comfort setpoints
                if isinstance(spec, FlatFilesSpec):
                    home_stp_cool = Conversions.C2Fx10(23.5)
                    home_stp_heat = Conversions.C2Fx10(21.0)
                    sleep_stp_cool = Conversions.C2Fx10(28.0)
                    sleep_stp_heat = Conversions.C2Fx10(16.5)
                elif isinstance(spec, DonateYourDataSpec):
                    home_stp_cool = Conversions.C2F(23.5)
                    home_stp_heat = Conversions.C2F(21.0)
                    sleep_stp_cool = Conversions.C2F(28.0)
                    sleep_stp_heat = Conversions.C2F(16.5)
                else:
                    home_stp_cool = 23.5
                    home_stp_heat = 21.0
                    sleep_stp_cool = 28.0
                    sleep_stp_heat = 16.5

                comfort_chg_pts = {
                    sim.start_utc: {
                        "Home": {
                            STATES.TEMPERATURE_STP_COOL: home_stp_cool,
                            STATES.TEMPERATURE_STP_HEAT: home_stp_heat,
                        },
                        "Sleep": {
                            STATES.TEMPERATURE_STP_COOL: sleep_stp_cool,
                            STATES.TEMPERATURE_STP_HEAT: sleep_stp_heat,
                        },
                    }
                }

            if not hvac_mode_chg_pts:
                # set default ecobee comfort setpoints
                hvac_mode_chg_pts = {sim.start_utc: "heat"}

            # enforce ascending sorting of dict keys
            hvac_mode_chg_pts = dict(sorted(hvac_mode_chg_pts.items()))
            comfort_chg_pts = dict(sorted(comfort_chg_pts.items()))
            schedule_chg_pts = dict(sorted(schedule_chg_pts.items()))

            # check for errors in settings
            if len(hvac_mode_chg_pts) <= 0:
                raise ValueError(
                    f"Invalid hvac_mode_chg_pts={hvac_mode_chg_pts}.")
            if len(comfort_chg_pts) <= 0:
                raise ValueError(f"Invalid comfort_chg_pts={comfort_chg_pts}.")
            if len(schedule_chg_pts) <= 0:
                raise ValueError(
                    f"Invalid schedule_chg_pts={schedule_chg_pts}.")

            for k, v in spec.full.spec.items():
                _default_value, _ = Conversions.numpy_down_cast_default_value_dtype(
                    v["dtype"])
                if v["channel"] == CHANNELS.THERMOSTAT_SETTING:
                    # settings channels set with default values first
                    # they are set below after full df columns have been filled
                    _df[k] = _default_value
                elif v["channel"] == CHANNELS.WEATHER:
                    # default: set no values for outdoor_weather=None
                    # will default to using TMY3 data for the provided location
                    if outdoor_weather:
                        # outdoor_weather can be set with internal states as keys
                        if v["internal_state"] in outdoor_weather.keys():
                            _df[k] = outdoor_weather[v["internal_state"]]

                elif v["channel"] == CHANNELS.THERMOSTAT_SENSOR:
                    # sensor data unused for dummy data
                    # set default
                    _df[k] = _default_value
                elif v["channel"] == CHANNELS.EQUIPMENT:
                    # equipment data unused for dummy data
                    # set default
                    _df[k] = _default_value

            # settings is always in spec add in specific order
            # 1. add HVAC_MODE
            k_hvac_mode = [
                k for k, v in spec.full.spec.items()
                if v["internal_state"] == STATES.HVAC_MODE
            ][0]
            # assuming sorted ascending by timestamp
            # each change point sets all future hvac modes
            for _ts, _hvac_mode in hvac_mode_chg_pts.items():
                _df.loc[_df.index >= _ts, k_hvac_mode] = _hvac_mode

            # 2. add SCHEDULE
            k_schedule = [
                k for k, v in spec.full.spec.items()
                if v["internal_state"] == STATES.SCHEDULE
            ][0]
            # assuming sorted ascending by timestamp
            # each change point sets all future schedules
            for _ts, _schedule in schedule_chg_pts.items():
                for _dow in range(7):
                    _dow_schedule = [
                        _s for _s in _schedule if _s["on_day_of_week"][_dow]
                    ]
                    _dow_schedule = sorted(_dow_schedule,
                                           key=lambda k: k["minute_of_day"])
                    _prev_dow_schedule = [
                        _s for _s in _schedule
                        if _s["on_day_of_week"][(_dow - 1) % 7]
                    ]
                    _prev_dow_schedule = sorted(
                        _prev_dow_schedule, key=lambda k: k["minute_of_day"])
                    # first period is defined from previous day of week last schedule
                    _prev_s = _prev_dow_schedule[-1]
                    _s = _dow_schedule[0]
                    _df.loc[(_df.index >= _ts)
                            & (_df.index.day_of_week == _dow)
                            & (_df.index.hour * 60 +
                               _df.index.minute < _s["minute_of_day"]),
                            k_schedule, ] = _prev_s["name"]
                    for _s in _dow_schedule:

                        _df.loc[(_df.index >= _ts)
                                & (_df.index.day_of_week == _dow)
                                & (_df.index.hour * 60 +
                                   _df.index.minute >= _s["minute_of_day"]),
                                k_schedule, ] = _s["name"]

            # 3. add SCHEDULE
            k_stp_cool = [
                k for k, v in spec.full.spec.items()
                if v["internal_state"] == STATES.TEMPERATURE_STP_COOL
            ][0]
            k_stp_heat = [
                k for k, v in spec.full.spec.items()
                if v["internal_state"] == STATES.TEMPERATURE_STP_HEAT
            ][0]
            # assuming sorted ascending by timestamp
            # each change point sets all future comfort set points
            for _ts, _comfort in comfort_chg_pts.items():
                for _schedule_name, _setpoints in _comfort.items():
                    _df.loc[(_df.index >= _ts) &
                            (_df[k_schedule] == _schedule_name),
                            k_stp_cool, ] = _setpoints[
                                STATES.TEMPERATURE_STP_COOL]
                    _df.loc[(_df.index >= _ts) &
                            (_df[k_schedule] == _schedule_name),
                            k_stp_heat, ] = _setpoints[
                                STATES.TEMPERATURE_STP_HEAT]

            _df = _df.reset_index().rename(
                columns={"index": spec.datetime_column})

            return _df
Ejemplo n.º 7
0
    def make_epw_file(
        self,
        sim_config,
        datetime_channel,
        epw_step_size_seconds,
    ):
        """Generate epw file in local time"""
        if self.epw_data.empty:
            raise ValueError(
                f"No input: epw_data={self.epw_data} and epw_path={self.epw_path}"
            )

        self.epw_step_size_seconds = epw_step_size_seconds

        _epw_path = os.path.join(
            self.simulation_epw_dir,
            "NREL_EPLUS" + f"_{sim_config['identifier']}" + f"_{self.epw_fname}",
        )

        # resample
        _cur_epw_data_period = (
            self.epw_data[self.datetime_column].diff().mode()[0].total_seconds()
        )
        if _cur_epw_data_period < self.epw_step_size_seconds:
            # downsample data
            non_numeric_cols = ["data_source_unct"]
            numeric_cols = [
                col for col in self.epw_data.columns if col not in non_numeric_cols
            ]

            self.epw_data = (
                self.epw_data.set_index(self.epw_data[self.datetime_column])
                .resample(f"{self.epw_step_size_seconds}S")
                .agg(
                    {
                        col: "mean" if col in numeric_cols else "max"
                        for col in self.epw_data.columns
                    }
                )
                .reset_index(drop=True)
            )
        elif _cur_epw_data_period > self.epw_step_size_seconds:
            # upsample data
            self.epw_data = self.epw_data.set_index(self.datetime_column)
            self.epw_data = self.epw_data.resample(
                f"{self.epw_step_size_seconds}S"
            ).asfreq()
            # ffill is only method that works on all types
            self.epw_data = self.epw_data.interpolate(axis="rows", method="ffill")
            self.epw_data = self.epw_data.reset_index()

        # NOTE:
        # EnergyPlus assumes solar radiance is given in W/m2 instead of Wh/m2
        # if more than one data interval per hour is given
        # see: https://github.com/NREL/EnergyPlus/blob/v9.4.0/src/EnergyPlus/WeatherManager.cc#L3147

        # compute dewpoint from dry-bulb and relative humidity
        self.epw_data["temp_dew"] = Conversions.relative_humidity_to_dewpoint(
            self.epw_data["temp_air"], self.epw_data["relative_humidity"]
        )

        # convert to local time INVARIANT to DST changes
        # .epw will have wrong hour columns if DST shift occurs during simulation
        # need a standard UTC offset for entire simulation period
        # no time zone shift occurs on or within 1 week of January 17th
        # use this for tz standard UTC offset
        tz_offset_seconds = datetime_channel.timezone.utcoffset(
            datetime(min(self.epw_data[self.datetime_column]).year, 1, 17)
        ).total_seconds()

        self.epw_data[self.datetime_column] = self.epw_data[
            self.datetime_column
        ] + pd.Timedelta(seconds=tz_offset_seconds)

        # last day of data must exist and be invariant to TZ shift
        # add ffill data for final day and extra day.
        _fill = self.epw_data.tail(1).copy(deep=True)
        _fill_rec = _fill.iloc[0]
        _fill[self.datetime_column] = _fill[self.datetime_column] + pd.Timedelta(
            days=2,
            hours=-_fill_rec[self.datetime_column].hour,
            minutes=-_fill_rec[self.datetime_column].minute,
            seconds=-_fill_rec[self.datetime_column].second,
        )
        self.epw_data = self.epw_data.append(_fill, ignore_index=True)
        self.epw_data = self.epw_data.set_index(self.datetime_column)

        # resample to building frequency
        self.epw_data = self.epw_data.resample(
            f"{self.epw_step_size_seconds}S"
        ).asfreq()
        # first ffill then bfill will fill both sides padding data
        self.epw_data = self.epw_data.fillna(method="ffill")
        self.epw_data = self.epw_data.fillna(method="bfill")
        self.epw_data = self.epw_data.reset_index()

        self.epw_data["year"] = self.epw_data[self.datetime_column].dt.year
        self.epw_data["month"] = self.epw_data[self.datetime_column].dt.month
        self.epw_data["day"] = self.epw_data[self.datetime_column].dt.day
        # energyplus uses non-standard hours [1-24] this is accounted in to_epw()
        self.epw_data["hour"] = self.epw_data[self.datetime_column].dt.hour
        self.epw_data["minute"] = self.epw_data[self.datetime_column].dt.minute

        # date time columns can be smaller dtypes
        self.epw_data = self.epw_data.astype(
            {
                "year": "Int16",
                "month": "Int8",
                "day": "Int8",
                "hour": "Int8",
                "minute": "Int8",
            },
        )

        meta_lines = self.add_epw_data_periods(
            epw_data=self.epw_data,
            meta_lines=self.epw_meta_lines,
            sim_config=sim_config,
        )

        # save to file
        self.to_epw(
            epw_data=self.epw_data,
            meta_lines=meta_lines,
            fpath=_epw_path,
        )

        self.epw_path = _epw_path

        return self.epw_path