class CostItem(odin.Resource): name = odin.StringField(null=True) code = odin.StringField(null=True) type = odin.StringField(null=True) season = odin.StringField(null=True) time = odin.StringField(null=True) cost = odin.FloatField()
class Book(odin.Resource): title = odin.StringField(name="Title") num_pages = odin.IntegerField(name="Num Pages") rrp = odin.FloatField(name="RRP") genre = odin.StringField(name="Genre", choices=( ('sci-fi', 'Science Fiction'), ('fantasy', 'Fantasy'), ('others', 'Others'), ), null=True) author = odin.StringField(name="Author") publisher = odin.StringField(name="Publisher") language = odin.StringField(name="Language", null=True) def extra_attrs(self, attrs): self.extras = attrs def __eq__(self, other): return (self.title == other.title and self.num_pages == other.num_pages and self.rrp == other.rrp and (self.genre == other.genre or (not self.genre and not other.genre)) and self.author == other.author and self.publisher == other.publisher and (self.language == other.language or (not self.language and not other.language)))
class Book(odin.Resource): title = odin.StringField(name="Title") num_pages = odin.IntegerField(name="Num Pages") rrp = odin.FloatField(name="RRP") genre = odin.StringField(name="Genre", choices=( ('sci-fi', 'Science Fiction'), ('fantasy', 'Fantasy'), ('others', 'Others'), )) author = odin.StringField(name="Author") publisher = odin.StringField(name="Publisher")
class Charge(odin.Resource): """A charge component of a tariff structure""" rate = odin.FloatField(null=True) rate_bands = odin.ArrayOf(RateBand, null=True) rate_schedule = odin.ArrayOf(ScheduleItem, null=True) time = odin.ObjectAs(Time, null=True) season = odin.ObjectAs(Season, null=True) type = odin.StringField(choices=CHARGE_TYPE_CHOICES, null=True, default='consumption', use_default_if_not_provided=True) meter = odin.StringField(null=True, default='electricity_imported', use_default_if_not_provided=True)
class OldBook(LibraryBook): name = odin.StringField(key=True) num_pages = odin.IntegerField() price = odin.FloatField() genre = odin.StringField(key=True, choices=( ('sci-fi', 'Science Fiction'), ('fantasy', 'Fantasy'), ('biography', 'Biography'), ('others', 'Others'), ('computers-and-tech', 'Computers & technology'), )) published = odin.DateTimeField() author = odin.ObjectAs(Author) publisher = odin.ObjectAs(Publisher)
class ToResource(odin.Resource): # Auto matched title = odin.StringField() count = odin.IntegerField() child = odin.ObjectAs(ChildResource) children = odin.ArrayOf(ChildResource) # Excluded excluded1 = odin.FloatField() # Mappings to_field1 = odin.StringField() to_field2 = odin.IntegerField() to_field3 = odin.IntegerField() same_but_different = odin.StringField() # Custom mappings to_field_c1 = odin.StringField() to_field_c2 = odin.StringField() to_field_c3 = odin.StringField() not_auto_c5 = odin.StringField() array_string = odin.TypedArrayField(odin.StringField()) assigned_field = odin.StringField()
class Charge(odin.Resource): """A charge component of a tariff structure""" code = odin.StringField(null=True) rate = odin.FloatField(null=True) rate_bands = odin.ArrayOf(RateBand, null=True) rate_schedule = odin.ArrayOf(ScheduleItem, null=True) time = odin.ObjectAs(Time, null=True) season = odin.ObjectAs(Season, null=True) type = odin.StringField(choices=CHARGE_TYPE_CHOICES, null=True, default='consumption', use_default_if_not_provided=True) meter = odin.StringField(null=True) @odin.calculated_field def name(self): season = self.season.name if self.season else None time = self.time.name if self.time else None scheduled = 'scheduled' if self.rate_schedule else None return str(self.code or '') + str(self.type or '') + str( season or '') + str(time or '') + str(scheduled or '')
class FromResource(odin.Resource): # Auto matched title = odin.StringField() count = odin.StringField() child = odin.ObjectAs(ChildResource) children = odin.ArrayOf(ChildResource) # Excluded excluded1 = odin.FloatField() # Mappings from_field1 = odin.StringField() from_field2 = odin.StringField() from_field3 = odin.IntegerField() from_field4 = odin.IntegerField() same_but_different = odin.StringField() # Custom mappings from_field_c1 = odin.StringField() from_field_c2 = odin.StringField() from_field_c3 = odin.StringField() from_field_c4 = odin.StringField() not_auto_c5 = odin.StringField() comma_separated_string = odin.StringField() # Virtual fields constant_field = odin.ConstantField(value=10)
class Book(LibraryBook): class Meta: key_field_name = 'isbn' title = odin.StringField() isbn = odin.StringField() num_pages = odin.IntegerField() rrp = odin.FloatField(default=20.4, use_default_if_not_provided=True) fiction = odin.BooleanField(is_attribute=True) genre = odin.StringField(choices=( ('sci-fi', 'Science Fiction'), ('fantasy', 'Fantasy'), ('biography', 'Biography'), ('others', 'Others'), ('computers-and-tech', 'Computers & technology'), )) published = odin.TypedArrayField(odin.DateTimeField()) authors = odin.ArrayOf(Author, use_container=True) publisher = odin.DictAs(Publisher, null=True) def __eq__(self, other): if other: return vars(self) == vars(other) return False
class RateBand(odin.Resource): """A specific band within a block pricing rate structure""" limit = odin.FloatField(null=True, default=9999999.9, use_default_if_not_provided=True) rate = odin.FloatField()
class Tariff(odin.Resource): """A collection of charges associated with a specific utility service""" name = odin.StringField(null=True) code = odin.StringField(null=True) utility_name = odin.StringField(null=True) utility_code = odin.StringField(null=True) service = odin.StringField(choices=SERVICE_CHOICES, null=True) sector = odin.StringField(choices=SECTOR_CHOICES, null=True) description = odin.StringField(null=True) currency = odin.StringField(null=True) timezone = odin.StringField(null=True) min_consumption = odin.FloatField(null=True) max_consumption = odin.FloatField(null=True) min_demand = odin.FloatField(null=True) max_demand = odin.FloatField(null=True) charges = odin.ArrayOf(Charge, null=True) monthly_charge = odin.FloatField(null=True) minimum_charge = odin.FloatField(null=True) times = odin.DictAs(Times, null=True) seasons = odin.DictAs(Seasons, null=True) net_metering = odin.BooleanField(null=True) billing_period = odin.StringField(choices=PERIOD_CHOICES, null=True, default='monthly', use_default_if_not_provided=True) demand_window = odin.StringField(choices=DEMAND_WINDOW_CHOICES, null=True, default='30min', use_default_if_not_provided=True) consumption_unit = odin.StringField(choices=CONSUMPTION_UNIT_CHOICES, null=True) demand_unit = odin.StringField(choices=DEMAND_UNIT_CHOICES, null=True) @odin.calculated_field def charge_types(self): charge_types = set() for charge in self.charges: if charge.season: charge_types.add('seasonal') if charge.time: charge_types.add('tou') if charge.rate_bands: charge_types.add('block') if charge.rate_schedule: charge_types.add('scheduled') if charge.type == 'demand': charge_types.add('demand') if charge.type == 'consumption': charge_types.add('consumption') return list(charge_types) def calc_charge(self, name, row, charge, charge_array, block_accum_dict): if charge.rate: charge_array[name].append(charge.rate * float(row[charge.meter])) if charge.rate_bands: charge_time_step = float() for rate_band_index, rate_band in enumerate(charge.rate_bands): if block_accum_dict[name] > rate_band.limit: continue block_usage = max((min( (rate_band.limit - block_accum_dict[name], row[charge.meter] - block_accum_dict[name])), 0.0)) charge_time_step += rate_band.rate * block_usage block_accum_dict[name] += block_usage charge_array[name].append(charge_time_step) return charge_array, block_accum_dict def apply_by_charge_type(self, meter_data, charge_type='consumption'): """ Calculates the cost of energy given a tariff and load. :param meter_data: a three-column pandas array with datetime, imported energy (kwh), exported energy (kwh) :param step: the time step in minutes of the meter data :param start: an optional datetime to select the commencement of the bill calculation :param end: an optional datetime to select the termination of the bill calculation :return: a dictionary containing the charge components (e.g. off_peak, shoulder, peak, total) """ charge_array = defaultdict(list) block_accum_dict = defaultdict(float) for dt, row in meter_data.iterrows(): time = datetime.time(hour=dt.hour, minute=dt.minute) # If the billing cycle changes over, reset block charge accumulations if (charge_type == 'consumption' and self.billing_period == 'monthly' and dt.day == 1 and dt.hour == 0 and dt.minute == 0 or self.billing_period == 'quarterly' and dt.month % 3 and dt.day == 1 and dt.hour == 0 and dt.minute == 0 or self.billing_period == 'annually' and dt.month == 1 and dt.day == 1 and dt.hour == 0 and dt.minute == 0): block_accum_dict = defaultdict(float) if charge_type == 'demand': block_accum_dict = defaultdict(float) if self.charges: for charge in self.charges: if charge.type == charge_type: if charge.time and charge.season: found = False if datetime.date(year=dt.year, month=charge.season.from_month, day=charge.season.from_day) \ < dt.date() <= datetime.date(year=dt.year, month=charge.season.to_month, day=charge.season.to_day): for period in charge.time.periods: if datetime.time( hour=period.from_hour, minute=period.from_minute ) < time <= datetime.time( hour=period.to_hour, minute=period.to_minute): charge_array, block_accum_dict = self.calc_charge( self.service + charge_type + charge.season.name + charge.time.name, row, charge, charge_array, block_accum_dict) found = True break if not found: charge_array[self.service + charge_type + charge.season.name + charge.time.name].append(0.0) elif charge.season and not charge.time: if datetime.date(year=dt.year, month=charge.season.from_month, day=charge.season.from_day)\ < dt.date() <= datetime.date(year=dt.year, month=charge.season.to_month, day=charge.season.to_day): charge_array, block_accum_dict = self.calc_charge( self.service + charge_type + charge.season.name, row, charge, charge_array, block_accum_dict) else: charge_array[self.service + charge_type + charge.season.name].append(0.0) elif charge.time and not charge.season: found = False for period in charge.time.periods: if datetime.time(hour=period.from_hour ) < time <= datetime.time( hour=period.to_hour): charge_array, block_accum_dict = self.calc_charge( self.service + charge_type + charge.time.name, row, charge, charge_array, block_accum_dict) found = True if not found: charge_array[self.service + charge_type + charge.time.name].append(0.0) elif charge.rate_schedule: for schedule_item in charge.rate_schedule: if dt <= schedule_item.datetime: charge_array[self.service + charge_type + 'scheduled'].append( schedule_item.rate * float(row[charge.meter])) break else: charge_array, block_accum_dict = self.calc_charge( self.service + charge_type, row, charge, charge_array, block_accum_dict) return charge_array def apply(self, meter_data, start=None, end=None, output_format='total'): """ Calculates the cost of energy given a tariff and load. :param meter_data: a three-column pandas array with datetime, imported energy (kwh), exported energy (kwh) :param step: the time step in minutes of the meter data :param start: an optional datetime to select the commencement of the bill calculation :param end: an optional datetime to select the termination of the bill calculation :return: a dictionary containing the charge components (e.g. off_peak, shoulder, peak, total) """ meter_data.truncate(before=start, after=end) charge_array = defaultdict(list) if 'consumption' in self.charge_types: consumption_data = meter_data # Resample meter data if finer resolution data not required by the specified tariff types if 'tou' not in self.charge_types and output_format != 'input-timestep' and output_format != 'input-timestep-components': if 'seasonal' in self.charge_types: consumption_data = meter_data.resample('D').sum() else: consumption_data = meter_data.resample( PERIOD_TO_TIMESTEP[self.billing_period]).sum() consumption_charges = self.apply_by_charge_type( consumption_data, 'consumption') charge_array.update(consumption_charges) if 'demand' in self.charge_types: if output_format == 'input-timestep' or output_format == 'input-timestep-components': raise UserWarning( "The output_format cannot be specified as 'input-timestep' if demand charges have " "been assigned.") # Resample meter data to the demand window and then take the maximum for each billing period demand_data = meter_data.resample( PERIOD_TO_TIMESTEP[self.demand_window]).mean() peak_monthly = demand_data.resample( PERIOD_TO_TIMESTEP[self.billing_period]).max() demand_charges = self.apply_by_charge_type(peak_monthly, 'demand') charge_array.update(demand_charges) # Transform the output data into the specified output format if output_format == 'total': output = 0.0 for v in charge_array.values(): output += sum(v) elif output_format == 'total-components': output = dict() for k, v in charge_array.items(): output[k] = sum(v) else: df = pandas.DataFrame.from_dict(data=charge_array) df.index = meter_data.index if output_format == 'billing-period': output = df.resample( PERIOD_TO_TIMESTEP[self.billing_period].sum()).sum(1) elif output_format == 'billing-period-components': output = df.resample( PERIOD_TO_TIMESTEP[self.billing_period].sum()) elif output_format == 'input-timestep': output = df.sum(1) elif output_format == 'input-timestep-components': output = df else: raise UserWarning('Unsupported output format: %s' % output_format) return output
class ScheduleItem(odin.Resource): """An item in a scheduled or real-time pricing rate structure""" datetime = odin.NaiveDateTimeField() rate = odin.FloatField()
class Tariff(odin.Resource): """A collection of charges associated with a specific utility service""" name = odin.StringField(null=True) code = odin.StringField(null=True) utility_name = odin.StringField(null=True) utility_code = odin.StringField(null=True) service = odin.StringField(choices=SERVICE_CHOICES, null=True) sector = odin.StringField(choices=SECTOR_CHOICES, null=True) description = odin.StringField(null=True) currency = odin.StringField(null=True) timezone = odin.StringField(null=True) min_consumption = odin.FloatField(null=True) max_consumption = odin.FloatField(null=True) min_demand = odin.FloatField(null=True) max_demand = odin.FloatField(null=True) charges = odin.ArrayOf(Charge, null=True) monthly_charge = odin.FloatField(null=True) minimum_charge = odin.FloatField(null=True) times = odin.DictAs(Times, null=True) seasons = odin.DictAs(Seasons, null=True) net_metering = odin.BooleanField(null=True) billing_period = odin.StringField(choices=PERIOD_CHOICES, null=True, default='monthly', use_default_if_not_provided=True) demand_window = odin.StringField(choices=DEMAND_WINDOW_CHOICES, null=True, default='30min', use_default_if_not_provided=True) consumption_unit = odin.StringField(choices=CONSUMPTION_UNIT_CHOICES, null=True) demand_unit = odin.StringField(choices=DEMAND_UNIT_CHOICES, null=True) @odin.calculated_field def charge_types(self): charge_types = set() for charge in self.charges: if charge.season: charge_types.add('seasonal') if charge.time: charge_types.add('tou') if charge.rate_bands: charge_types.add('block') if charge.rate_schedule: charge_types.add('scheduled') if charge.type == 'demand': charge_types.add('demand') if charge.type == 'consumption': charge_types.add('consumption') if charge.type == 'generation': charge_types.add('generation') if charge.type == 'fixed': charge_types.add('fixed') return list(charge_types) def calc_charge(self, row, charge, cost_items, block_accum_dict): if charge.rate: try: cost_items[charge.name]['cost'] += charge.rate * float( row[charge.meter or charge.type]) except KeyError: raise Exception( "The specified meter data file has no field named %s" % str(charge.meter or charge.type)) if charge.rate_bands: charge_time_step = float() prev_block_usage = 0.0 for rate_band_index, rate_band in enumerate(charge.rate_bands): if block_accum_dict[charge.name] > rate_band.limit: continue try: block_usage = max((min( (rate_band.limit - block_accum_dict[charge.name], row[charge.meter or charge.type] - prev_block_usage)), 0.0)) except KeyError: raise Exception( "The specified meter data file has no field named %s" % str(charge.meter or charge.type)) prev_block_usage += block_usage charge_time_step += rate_band.rate * block_usage block_accum_dict[charge.name] += block_usage cost_items[charge.name]['cost'] += charge_time_step return cost_items, block_accum_dict def apply_by_charge_type(self, meter_data, cost_items, charge_type='consumption'): """ Calculates the cost of energy given a tariff and load. :param meter_data: a three-column pandas array with datetime, imported energy (kwh), exported energy (kwh) :param cost_items: a dictionary containing the charge components (e.g. off_peak, shoulder, peak, total) :param charge_type: a string specifying the charge type, options include 'consumption', 'demand' etc :return: a dictionary containing the charge components (e.g. off_peak, shoulder, peak, total) """ block_accum_dict = defaultdict(float) for dt, row in meter_data.iterrows(): time = datetime.time(hour=dt.hour, minute=dt.minute) # If the billing cycle changes over, reset block charge accumulations if (charge_type == 'consumption' and self.billing_period == 'monthly' and dt.day == 1 and dt.hour == 0 and dt.minute == 0 or self.billing_period == 'quarterly' and dt.month % 3 and dt.day == 1 and dt.hour == 0 and dt.minute == 0 or self.billing_period == 'annually' and dt.month == 1 and dt.day == 1 and dt.hour == 0 and dt.minute == 0): block_accum_dict = defaultdict(float) if charge_type == 'demand': block_accum_dict = defaultdict(float) if self.charges: for charge in self.charges: if charge.type == charge_type or ( charge_type == 'consumption' and charge.type == 'generation'): if charge.time and charge.season: if datetime.date(year=dt.year, month=charge.season.from_month, day=charge.season.from_day) \ <= dt.date() <= datetime.date(year=dt.year, month=charge.season.to_month, day=charge.season.to_day): for period in charge.time.periods: if period.from_weekday <= dt.dayofweek <= period.to_weekday and datetime.time( hour=period.from_hour, minute=period.from_minute ) <= time <= datetime.time( hour=period.to_hour, minute=period.to_minute): cost_items, block_accum_dict = self.calc_charge( row, charge, cost_items, block_accum_dict) break elif charge.season: if datetime.date(year=dt.year, month=charge.season.from_month, day=charge.season.from_day)\ <= dt.date() <= datetime.date(year=dt.year, month=charge.season.to_month, day=charge.season.to_day): cost_items, block_accum_dict = self.calc_charge( row, charge, cost_items, block_accum_dict) elif charge.time: for period in charge.time.periods: if period.from_weekday <= dt.dayofweek <= period.to_weekday and datetime.time( hour=period.from_hour, minute=period.from_minute ) <= time <= datetime.time( hour=period.to_hour, minute=period.to_minute): cost_items, block_accum_dict = self.calc_charge( row, charge, cost_items, block_accum_dict) elif charge.rate_schedule: for schedule_item in charge.rate_schedule: if dt.to_pydatetime() < schedule_item.datetime: cost_items[charge.type + 'scheduled'][ 'cost'] += schedule_item.rate * float( row[charge.type]) break else: cost_items, block_accum_dict = self.calc_charge( row, charge, cost_items, block_accum_dict) return cost_items def apply(self, meter_data, start=None, end=None): """ Calculates the cost of energy given a tariff and load. :param meter_data: a three-column pandas array with datetime, imported energy (kwh), exported energy (kwh) :param start: an optional datetime to select the commencement of the bill calculation :param end: an optional datetime to select the termination of the bill calculation :return: a dictionary containing the charge components (e.g. off_peak, shoulder, peak, total) """ meter_data.truncate(before=start, after=end) # Create empty dict to hold calculated cost items cost_items = dict() for charge in self.charges: season = charge.season.name if charge.season else None time = charge.time.name if charge.time else None cost_items[charge.name] = { 'name': charge.name, 'code': charge.code, 'type': charge.type, 'season': season, 'time': time, 'cost': 0.0, } if 'consumption' in self.charge_types or 'generation' in self.charge_types: consumption_data = meter_data # Resample meter data if finer resolution data not required by the specified tariff types if 'tou' not in self.charge_types: if 'seasonal' in self.charge_types: consumption_data = meter_data.resample('D').sum() else: consumption_data = meter_data.resample( PERIOD_TO_TIMESTEP[self.billing_period]).sum() cost_items.update( self.apply_by_charge_type(consumption_data, cost_items, 'consumption')) if 'demand' in self.charge_types: # Resample meter data to the demand window and then take the maximum for each billing period demand_data = meter_data.resample( PERIOD_TO_TIMESTEP[self.demand_window]).mean() peak_monthly = demand_data.resample( PERIOD_TO_TIMESTEP[self.billing_period]).max() cost_items.update( self.apply_by_charge_type(peak_monthly, cost_items, 'demand')) if 'fixed' in self.charge_types: billing_periods = len( meter_data.resample( PERIOD_TO_TIMESTEP[self.billing_period]).mean()) for charge in self.charges: if charge.type == 'fixed': cost_items[ charge.name]['cost'] = billing_periods * charge.rate # Transform the output data into the specified output format cost_dict = { 'name': self.name, 'code': self.code, 'items': cost_items.values() } return dict_codec.load(cost_dict, Cost)