Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not init facade in adapter #47

Merged
merged 9 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have had this Version with facade as DataAdapter attribute before but I do not remember why we changed that.


@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
Loading