Skip to content

Commit

Permalink
Merge pull request #47 from sedos-project/feature/do_not_init_facade_…
Browse files Browse the repository at this point in the history
…in_adapter

Do not init facade in adapter
  • Loading branch information
henhuy authored Oct 6, 2023
2 parents 7462d6d + e539037 commit c021c23
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 161 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea/
collections/*
tests/collections/
__pycache__/
#/structures/
/structures/
136 changes: 42 additions & 94 deletions data_adapter_oemof/adapters.py
Original file line number Diff line number Diff line change
@@ -1,56 +1,32 @@
import dataclasses
import logging

from oemof.solph import Bus
from oemof.tabular import facades
from oemof.tabular._facade import Facade

from data_adapter_oemof import calculations
from data_adapter_oemof.mappings import Mapper
from data_adapter_oemof.mappings import Field, Mapper

logger = logging.getLogger()


class Adapter:
type: str = "adapter"
extra_attributes = ("name", "type", "year")

def as_dict(self):
"""
Adds function to return DataFrame from adapter.
This mixin is needed because `pd.DataFrame(dataclass_instance)` creates columns
only for existing dataclass attributes.
But we add custom_attributes (i.e. "name") which would be neglected.
"""
fields = dataclasses.fields(self)
data = {field.name: getattr(self, field.name) for field in fields}
for attr in self.extra_attributes:
data[attr] = getattr(self, attr)
return data

@classmethod
def parametrize_dataclass(
cls,
struct,
mapper: Mapper,
) -> "Adapter":
return cls(**cls.get_default_parameters(struct, mapper))

@classmethod
def get_default_parameters(cls, struct: dict, mapper: Mapper) -> dict:
defaults = {
"type": cls.type,
}
facade: Facade = None
extra_fields = (
Field(name="name", type=str),
Field(name="type", type=str),
Field(name="region", type=str),
Field(name="year", type=int),
)

def __init__(self, struct: dict, mapper: Mapper):
self.facade_dict = self.get_default_parameters(struct, mapper)

def get_default_parameters(self, struct: dict, mapper: Mapper) -> dict:
defaults = {"type": self.type}
# Add mapped attributes
mapped_values = mapper.get_default_mappings(struct)
defaults.update(mapped_values)
# Add additional attributes

attributes = {
"region": mapper.get("region"),
"year": mapper.get("year"),
}
defaults.update(attributes)

# add name if found in data, else use calculation for name:
if (name := mapper.get("name")) is not None:
Expand All @@ -67,146 +43,118 @@ def get_default_parameters(cls, struct: dict, mapper: Mapper) -> dict:
return defaults


def facade_adapter(cls):
r"""
Sets type of Busses to str
Sets 'build_solph_components' to False in __init__.
"""
# set type of bus to str
for field in dataclasses.fields(cls):
if field.type == Bus:
field.type = str

original_init = cls.__init__

# do not build solph components
def new_init(self, *args, **kwargs):
custom_attributes = {}
original_fields = tuple(field.name for field in dataclasses.fields(cls))
for key in tuple(kwargs.keys()):
if key not in original_fields:
custom_attributes[key] = kwargs.pop(key)

original_init(self, *args, build_solph_components=False, **kwargs)

for key, value in custom_attributes.items():
setattr(self, key, value)

cls.__init__ = new_init

return cls


@facade_adapter
class DispatchableAdapter(facades.Dispatchable, Adapter):
class DispatchableAdapter(Adapter):
"""
Dispatchable Adapter
"""

type = "dispatchable"
facade = facades.Dispatchable


@facade_adapter
class HeatPumpAdapter(facades.HeatPump, Adapter):
class HeatPumpAdapter(Adapter):
"""
HeatPump Adapter
"""

type = "heat_pump"
facade = facades.HeatPump


@facade_adapter
class LinkAdapter(facades.Link, Adapter):
class LinkAdapter(Adapter):
"""
Link Adapter
"""

type = "link"
facade = facades.Link


@facade_adapter
class ReservoirAdapter(facades.Reservoir, Adapter):
class ReservoirAdapter(Adapter):
"""
Reservoir Adapter
"""

type = "reservoir"
facade = facades.Reservoir


@facade_adapter
class ExcessAdapter(facades.Excess, Adapter):
class ExcessAdapter(Adapter):
"""
Excess Adapter
"""

type = "excess"
facade = facades.Excess


@facade_adapter
class BackpressureTurbineAdapter(facades.BackpressureTurbine, Adapter):
class BackpressureTurbineAdapter(Adapter):
"""
BackpressureTurbine Adapter
"""

type = "backpressure_turbine"
facade = facades.BackpressureTurbine


@facade_adapter
class CommodityAdapter(facades.Commodity, Adapter):
class CommodityAdapter(Adapter):
"""
CommodityAdapter
"""

type = "commodity"
facade = facades.Commodity


@facade_adapter
class ConversionAdapter(facades.Conversion, Adapter):
class ConversionAdapter(Adapter):
"""
ConversionAdapter
To use Conversion, map the inputs and outputs within the structure to avoid deduction failure.
"""

type = "conversion"
facade = facades.Conversion


@facade_adapter
class LoadAdapter(facades.Load, Adapter):
class LoadAdapter(Adapter):
"""
LoadAdapter
"""

type = "load"
facade = facades.Load


@facade_adapter
class StorageAdapter(facades.Storage, Adapter):
class StorageAdapter(Adapter):
"""
StorageAdapter
"""

type = "storage"
facade = facades.Storage


@facade_adapter
class ExtractionTurbineAdapter(facades.ExtractionTurbine, Adapter):
class ExtractionTurbineAdapter(Adapter):
"""
ExtractionTurbineAdapter
"""

type = "extraction_turbine"
facade = facades.ExtractionTurbine


@facade_adapter
class VolatileAdapter(facades.Volatile, Adapter):
class VolatileAdapter(Adapter):
"""
VolatileAdapter
"""

type = "volatile"
facade = facades.Volatile


# Create a dictionary of all adapter classes defined in this module
FACADE_ADAPTERS = {
name: adapter for name, adapter in globals().items() if name.endswith("Adapter")
adapter.type: adapter
for name, adapter in globals().items()
if name.endswith("Adapter")
}
9 changes: 5 additions & 4 deletions data_adapter_oemof/build_datapackage.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def get_foreign_keys(struct: list, mapper: Mapper, components: list) -> list:
{"fields": bus, "reference": {"fields": "name", "resource": "bus"}}
)

for field in dataclasses.fields(mapper.adapter):
for field in mapper.get_fields():
if (
mapper.is_sequence(field.type)
and field.name in components.columns
Expand Down Expand Up @@ -393,10 +393,11 @@ def build_datapackage(cls, adapter: Adapter):
data=component_data,
timeseries=timeseries,
)
component = facade_adapter.parametrize_dataclass(
struct, component_mapper
components.append(
FACADE_ADAPTERS[facade_adapter_name](
struct=struct, mapper=component_mapper
).facade_dict
)
components.append(component.as_dict())
# Fill with all busses occurring, needed for foreign keys as well!
process_busses += list(component_mapper.get_busses(struct).values())

Expand Down
15 changes: 11 additions & 4 deletions data_adapter_oemof/mappings.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import collections
import dataclasses
import difflib
import logging
Expand All @@ -15,6 +16,8 @@
"tech": "tech",
}

Field = collections.namedtuple(typename="Field", field_names=["name", "type"])


class MappingError(Exception):
"""Raised if mapping fails"""
Expand All @@ -41,6 +44,12 @@ def __init__(
self.mapping = mapping
self.bus_map = bus_map

def get_fields(self):
return [
Field(name=field.name, type=field.type)
for field in dataclasses.fields(self.adapter.facade)
] + list(self.adapter.extra_fields)

def map_key(self, key):
"""Use adapter specific mapping if available, otherwise use default
mapping or return key if no mapping is available.
Expand Down Expand Up @@ -143,9 +152,7 @@ def get_busses(self, struct):
:return: dictionary with tabular like Busses
"""
bus_occurrences_in_fields = [
field.name
for field in dataclasses.fields(self.adapter)
if "bus" in field.name
field.name for field in self.get_fields() if "bus" in field.name
]
if len(bus_occurrences_in_fields) == 0:
logger.warning(
Expand Down Expand Up @@ -213,7 +220,7 @@ def get_default_mappings(self, struct):

mapped_all_class_fields = {
field.name: value
for field in dataclasses.fields(self.adapter)
for field in self.get_fields()
if (value := self.get(field.name, field.type)) is not None
}
mapped_all_class_fields.update(self.get_busses(struct))
Expand Down
6 changes: 3 additions & 3 deletions data_adapter_oemof/mappings/PROCESS_TYPE_MAP.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This is a mapping file to map the various processes to the oemof tabular facades.
modex_tech_wind_turbine_onshore: VolatileAdapter
modex_tech_storage_battery: StorageAdapter
modex_tech_generator_gas: ConversionAdapter
modex_tech_wind_turbine_onshore: volatile
modex_tech_storage_battery: storage
modex_tech_generator_gas: conversion
Loading

0 comments on commit c021c23

Please sign in to comment.