class SourceComponentBase(odin.Resource): """Base class for archival components. Subclassed by SourceArchivalObject and SourceResource. Both language and lang_material need to exist in order to accomodate ArchivesSpace API changes between v2.6 and v2.7. """ class Meta: abstract = True COMPONENT_TYPES = (('archival_object', 'Archival Object'), ('resource', 'Resource')) dates = odin.ArrayOf(SourceDate) extents = odin.ArrayOf(SourceExtent) external_ids = odin.ArrayOf(SourceExternalId) group = odin.DictAs(SourceGroup) jsonmodel_type = odin.StringField(choices=COMPONENT_TYPES) lang_materials = odin.ArrayOf(SourceLangMaterial, null=True) language = odin.StringField(null=True) level = odin.StringField() linked_agents = odin.ArrayOf(SourceLinkedAgent) notes = odin.ArrayOf(SourceNote) publish = odin.BooleanField() subjects = odin.ArrayOf(SourceRef) suppressed = odin.StringField() title = odin.StringField(null=True) uri = odin.StringField()
class Level1(odin.Resource): class Meta: namespace = 'odin.traversal' name = odin.StringField() level2 = odin.DictAs(Level2) level2s = odin.DictOf(Level2)
class SourceTransfer(odin.Resource): metadata = odin.DictAs(SourceMetadata) url = odin.StringField() rights_statements = odin.ArrayOf(SourceRightsStatement) resource = odin.StringField() parent = odin.StringField(null=True) linked_agents = odin.ArrayOf(SourceLinkedCreator, null=True) level = odin.StringField()
class SourceSubject(odin.Resource): """A topical term.""" external_ids = odin.ArrayOf(SourceExternalId) group = odin.DictAs(SourceGroup) publish = odin.BooleanField() source = odin.StringField(choices=configs.SUBJECT_SOURCE_CHOICES) terms = odin.ArrayOf(SourceTerm) title = odin.StringField() uri = odin.StringField()
class SourceArchivalObject(SourceComponentBase): """A component of a SourceResource.""" position = odin.IntegerField() ref_id = odin.StringField() component_id = odin.StringField(null=True) display_string = odin.StringField() restrictions_apply = odin.BooleanField() ancestors = odin.ArrayOf(SourceAncestor) resource = odin.DictAs(SourceRef) has_unpublished_ancestor = odin.BooleanField() instances = odin.ArrayOf(SourceInstance)
class SourceAgentBase(odin.Resource): """A base class for agents. Subclassed by SourceAgentFamily, SourceAgentPerson and SourceAgentCorporateEntity. """ class Meta: abstract = True AGENT_TYPES = (('agent_corporate_entity', 'Organization'), ('agent_family', 'Family'), ('agent_person', 'Person')) agent_record_identifiers = odin.ArrayOf(SourceAgentRecordIdentifier, null=True) dates_of_existence = odin.ArrayField(null=True) group = odin.DictAs(SourceGroup) jsonmodel_type = odin.StringField(choices=AGENT_TYPES) notes = odin.ArrayOf(SourceNote) publish = odin.BooleanField() title = odin.StringField() uri = odin.StringField()
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 ArchivesSpaceArchivalObject(ArchivesSpaceComponentBase): """Groups of records that are part of collections.""" language = odin.StringField(null=True) level = odin.StringField(choices=resource_configs.LEVEL_CHOICES) resource = odin.DictAs(ArchivesSpaceRef) parent = odin.DictField(null=True)
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 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)
class SourceStructuredDate(odin.Resource): """An alternative representation of dates, currently associated only with agents.""" date_label = odin.StringField(choices=configs.DATE_LABEL_CHOICES) date_type_structured = odin.StringField(choices=configs.DATE_TYPE_CHOICES) structured_date_single = odin.DictAs(SourceStructuredDateSingle, null=True) structured_date_range = odin.DictAs(SourceStructuredDateRange, null=True)
class SourceInstance(odin.Resource): """The physical or digital instantiation of a group of records.""" instance_type = odin.StringField(choices=configs.INSTANCE_TYPE_CHOICES) is_representative = odin.BooleanField() sub_container = odin.DictAs(SourceSubcontainer, null=True) digital_object = odin.DictAs(SourceRef, null=True)
class SourceAgentFamily(SourceAgentBase): """A family.""" names = odin.ArrayOf(SourceNameFamily) display_name = odin.DictAs(SourceNameFamily)
class SourceAgentCorporateEntity(SourceAgentBase): """An organization.""" names = odin.ArrayOf(SourceNameCorporateEntity) display_name = odin.DictAs(SourceNameCorporateEntity)
class ArchivesSpaceLangMaterial(odin.Resource): """Records information about the languages of archival records. Applies to resources post-ArchivesSpace v2.7 only. """ language_and_script = odin.DictAs(ArchivesSpaceLanguageAndScript, null=True)
class SourceAgentPerson(SourceAgentBase): """A person.""" names = odin.ArrayOf(SourceNamePerson) display_name = odin.DictAs(SourceNamePerson)
class SourceSubcontainer(odin.Resource): """Provides detailed container information.""" indicator_2 = odin.StringField(null=True) type_2 = odin.StringField(choices=configs.CONTAINER_TYPE_CHOICES, null=True) top_container = odin.DictAs(SourceRef)