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

feat: new PfS columns #429

Merged
merged 20 commits into from
Apr 18, 2024
Merged
Show file tree
Hide file tree
Changes from 15 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
8 changes: 8 additions & 0 deletions src/libecalc/dto/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,14 @@ class GeneratorSet(BaseEquipment):
Field(discriminator="component_type"),
]
] = Field(default_factory=list)
cable_loss: Optional[ExpressionType] = Field(
None,
title="CABLE_LOSS",
description="Power loss in cables from shore. " "Used to calculate onshore delivery/power supply onshore.",
)
max_usage_from_shore: Optional[ExpressionType] = Field(
None, title="MAX_USAGE_FROM_SHORE", description="The peak load/effect that is expected for one hour, per year."
)
_validate_genset_temporal_models = field_validator("generator_set_model", "fuel")(validate_temporal_model)

@field_validator("user_defined_category", mode="before")
Expand Down
3 changes: 3 additions & 0 deletions src/libecalc/fixtures/cases/ltp_export/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

from libecalc.fixtures import YamlCase
from libecalc.fixtures.case_utils import YamlCaseLoader
from libecalc.fixtures.cases.ltp_export.ltp_power_from_shore_yaml import (
ltp_pfs_yaml_factory,
)

"""
Test project for LTP Export
Expand Down
11 changes: 11 additions & 0 deletions src/libecalc/fixtures/cases/ltp_export/data/sim/cable_loss.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
DATE,CABLE_LOSS_FACTOR
01.01.2021,0.1
01.01.2022,0.1
01.01.2023,0.1
01.01.2024,0.1
01.01.2025,0.1
01.01.2026,0.1
01.01.2027,0.1
01.01.2028,0.1
01.01.2029,0.1
01.01.2030,0.1
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from pathlib import Path

import pytest
import yaml

from libecalc.common.time_utils import Frequency
from libecalc.dto import ResultOptions
from libecalc.expression.expression import ExpressionType
from libecalc.fixtures.case_types import DTOCase
from libecalc.presentation.yaml.mappers.variables_mapper import map_yaml_to_variables
from libecalc.presentation.yaml.model import PyYamlYamlModel, YamlModel
from libecalc.presentation.yaml.parse_input import map_yaml_to_dto


@pytest.fixture
def ltp_pfs_yaml_factory():
def _ltp_pfs_yaml_factory(
regularity: float,
cable_loss: ExpressionType,
max_usage_from_shore: ExpressionType,
load_direct_consumer: float,
path: Path,
) -> DTOCase:
input_text = f"""
START: 2025-01-01
END: 2030-01-01
TIME_SERIES:
- NAME: CABLE_LOSS
TYPE: DEFAULT
FILE: data/sim/cable_loss.csv
FACILITY_INPUTS:
- NAME: generator_energy_function
FILE: 'data/einput/genset_17MW.csv'
TYPE: ELECTRICITY2FUEL
- NAME: pfs_energy_function
FILE: 'data/einput/onshore_power.csv'
TYPE: ELECTRICITY2FUEL
FUEL_TYPES:
- NAME: fuel1
EMISSIONS:
- NAME: co2
FACTOR: 2
- NAME: ch4
FACTOR: 0.005
- NAME: nmvoc
FACTOR: 0.002
- NAME: nox
FACTOR: 0.001

INSTALLATIONS:
- NAME: minimal_installation
HCEXPORT: 0
FUEL: fuel1
CATEGORY: FIXED
REGULARITY: {regularity}
GENERATORSETS:
- NAME: generator1
ELECTRICITY2FUEL:
2025-01-01: generator_energy_function
2027-01-01: pfs_energy_function
CATEGORY:
2025-01-01: TURBINE-GENERATOR
2027-01-01: POWER-FROM-SHORE
CABLE_LOSS: {cable_loss}
MAX_USAGE_FROM_SHORE: {max_usage_from_shore}

CONSUMERS: # electrical energy consumers
- NAME: base_load
CATEGORY: BASE-LOAD
ENERGY_USAGE_MODEL:
TYPE: DIRECT
LOAD: {load_direct_consumer}


"""

yaml_text = yaml.safe_load(input_text)
configuration = PyYamlYamlModel(
internal_datamodel=yaml_text,
instantiated_through_read=True,
)

path = path

resources = YamlModel._read_resources(yaml_configuration=configuration, working_directory=path)
variables = map_yaml_to_variables(
configuration,
resources=resources,
result_options=ResultOptions(
start=configuration.start,
end=configuration.end,
output_frequency=Frequency.YEAR,
),
)

yaml_model = map_yaml_to_dto(configuration=configuration, resources=resources, name="ltp_export")
return DTOCase(ecalc_model=yaml_model, variables=variables)

return _ltp_pfs_yaml_factory
18 changes: 18 additions & 0 deletions src/libecalc/presentation/exporter/configs/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
EmissionQuery,
FuelConsumerPowerConsumptionQuery,
FuelQuery,
MaxUsageFromShoreQuery,
PowerSupplyOnshoreQuery,
)

"""
Expand Down Expand Up @@ -634,6 +636,22 @@ def aggregator(frequency: Frequency) -> Aggregator:
producer_categories=["POWER-FROM-SHORE"],
),
),
Applier(
name="powerSupplyOnshore",
title="Power Supply Onshore",
unit=Unit.GIGA_WATT_HOURS,
query=PowerSupplyOnshoreQuery(
producer_categories=["POWER-FROM-SHORE"],
),
),
Applier(
name="fromShorePeakMaximum",
title="Max Usage from Shore",
unit=Unit.GIGA_WATT_HOURS,
query=MaxUsageFromShoreQuery(
producer_categories=["POWER-FROM-SHORE"],
),
),
Applier(
name="steamTurbineGeneratorConsumption",
title="Total Electricity Consumed From Steam Turbine Generators",
Expand Down
169 changes: 169 additions & 0 deletions src/libecalc/presentation/exporter/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
import libecalc.dto
from libecalc.application.graph_result import GraphResult
from libecalc.common.decorators.feature_flags import Feature
from libecalc.common.list.list_utils import array_to_list
from libecalc.common.temporal_model import TemporalExpression, TemporalModel
from libecalc.common.time_utils import Frequency, resample_time_steps
from libecalc.common.units import Unit
from libecalc.common.utils.rates import (
TimeSeriesFloat,
TimeSeriesRate,
TimeSeriesStreamDayRate,
TimeSeriesVolumes,
)
from libecalc.core.result import GeneratorSetResult
from libecalc.expression import Expression


class Query(abc.ABC):
Expand Down Expand Up @@ -308,6 +311,172 @@ def query(
return aggregated_result_volume if aggregated_result_volume else None


class PowerSupplyOnshoreQuery(Query):
"""GenSet only (ie el producers)."""

def __init__(self, installation_category: Optional[str] = None, producer_categories: Optional[List[str]] = None):
self.installation_category = installation_category
self.producer_categories = producer_categories

def query(
self,
installation_graph: GraphResult,
unit: Unit,
frequency: Frequency,
) -> Optional[Dict[datetime, float]]:
installation_dto = installation_graph.graph.get_node(installation_graph.graph.root)

installation_time_steps = installation_graph.timesteps
time_steps = resample_time_steps(
frequency=frequency,
time_steps=installation_time_steps,
)

regularity = TimeSeriesFloat(
timesteps=installation_time_steps,
values=TemporalExpression.evaluate(
temporal_expression=TemporalModel(installation_dto.regularity),
variables_map=installation_graph.variables_map,
),
unit=Unit.NONE,
)

aggregated_result: DefaultDict[datetime, float] = defaultdict(float)
aggregated_result_volume = {}
unit_in = None

if self.installation_category is None or installation_dto.user_defined_category == self.installation_category:
for fuel_consumer in installation_dto.fuel_consumers:
if isinstance(fuel_consumer, libecalc.dto.GeneratorSet) and fuel_consumer.cable_loss is not None:
temporal_category = TemporalModel(fuel_consumer.user_defined_category)
for period, category in temporal_category.items():
if self.producer_categories is None or category in self.producer_categories:
fuel_consumer_result: GeneratorSetResult = installation_graph.get_energy_result(
fuel_consumer.id
)

cable_loss = Expression.evaluate(
fuel_consumer.cable_loss,
variables=installation_graph.variables_map.variables,
fill_length=len(installation_graph.variables_map.time_vector),
)

fuel_consumer_result.power.values = fuel_consumer_result.power.values * (1 + cable_loss)

cumulative_volumes_gwh = (
TimeSeriesRate.from_timeseries_stream_day_rate(
fuel_consumer_result.power, regularity=regularity
)
.for_period(period)
.to_volumes()
)

unit_in = cumulative_volumes_gwh.unit

for timestep, cumulative_volume_gwh in cumulative_volumes_gwh.datapoints():
aggregated_result[timestep] += cumulative_volume_gwh

if aggregated_result:
sorted_result = dict(dict(sorted(zip(aggregated_result.keys(), aggregated_result.values()))).items())
sorted_result = {**dict.fromkeys(installation_time_steps, 0.0), **sorted_result}
date_keys = list(sorted_result.keys())

reindexed_result = (
TimeSeriesVolumes(timesteps=date_keys, values=list(sorted_result.values())[:-1], unit=unit_in)
.to_unit(Unit.GIGA_WATT_HOURS)
.reindex(time_steps)
.fill_nan(0)
)

aggregated_result_volume = {
reindexed_result.timesteps[i]: reindexed_result.values[i] for i in range(len(reindexed_result))
}
return aggregated_result_volume if aggregated_result_volume else None


class MaxUsageFromShoreQuery(Query):
"""GenSet only (ie el producers)."""

def __init__(self, installation_category: Optional[str] = None, producer_categories: Optional[List[str]] = None):
self.installation_category = installation_category
self.producer_categories = producer_categories

def query(
self,
installation_graph: GraphResult,
unit: Unit,
frequency: Frequency,
) -> Optional[Dict[datetime, float]]:
installation_dto = installation_graph.graph.get_node(installation_graph.graph.root)

installation_time_steps = installation_graph.timesteps
time_steps = resample_time_steps(
frequency=frequency,
time_steps=installation_time_steps,
)

regularity = TimeSeriesFloat(
timesteps=installation_time_steps,
values=TemporalExpression.evaluate(
temporal_expression=TemporalModel(installation_dto.regularity),
variables_map=installation_graph.variables_map,
),
unit=Unit.NONE,
)

aggregated_result: DefaultDict[datetime, float] = defaultdict(float)
aggregated_result_volume = {}
unit_in = None

if self.installation_category is None or installation_dto.user_defined_category == self.installation_category:
for fuel_consumer in installation_dto.fuel_consumers:
if (
isinstance(fuel_consumer, libecalc.dto.GeneratorSet)
and fuel_consumer.max_usage_from_shore is not None
):
temporal_category = TemporalModel(fuel_consumer.user_defined_category)
for period, category in temporal_category.items():
if self.producer_categories is None or category in self.producer_categories:
installation_graph.get_energy_result(fuel_consumer.id)

max_usage_from_shore = TimeSeriesStreamDayRate(
values=array_to_list(
Expression.evaluate(
fuel_consumer.max_usage_from_shore,
variables=installation_graph.variables_map.variables,
fill_length=len(installation_graph.variables_map.time_vector),
)
),
unit=Unit.MEGA_WATT,
timesteps=installation_graph.variables_map.time_vector,
)

results = TimeSeriesRate.from_timeseries_stream_day_rate(
max_usage_from_shore, regularity=regularity
).for_period(period)

unit_in = results.unit

for timestep, result in results.datapoints():
aggregated_result[timestep] += result

if aggregated_result:
sorted_result = dict(dict(sorted(zip(aggregated_result.keys(), aggregated_result.values()))).items())
sorted_result = {**dict.fromkeys(installation_time_steps, 0.0), **sorted_result}
date_keys = list(sorted_result.keys())

reindexed_result = (
TimeSeriesVolumes(timesteps=date_keys, values=list(sorted_result.values())[:-1], unit=unit_in)
.reindex(time_steps)
.fill_nan(0)
)

aggregated_result_volume = {
reindexed_result.timesteps[i]: reindexed_result.values[i] for i in range(len(reindexed_result))
}
return aggregated_result_volume if aggregated_result_volume else None


class FuelConsumerPowerConsumptionQuery(Query):
def __init__(self, consumer_categories: Optional[List[str]] = None, installation_category: Optional[str] = None):
self.consumer_categories = consumer_categories
Expand Down
18 changes: 14 additions & 4 deletions src/libecalc/presentation/yaml/mappers/component_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from libecalc import dto
from libecalc.common.logger import logger
from libecalc.common.time_utils import Period, define_time_model_for_period
from libecalc.dto.base import ComponentType
from libecalc.dto.base import ComponentType, ConsumerUserDefinedCategoryType
from libecalc.dto.types import ConsumerType, ConsumptionType, EnergyModelType
from libecalc.dto.utils.validators import convert_expression
from libecalc.expression import Expression
Expand Down Expand Up @@ -216,6 +216,16 @@ def from_yaml_to_dto(
)
for consumer in data.get(EcalcYamlKeywords.consumers, [])
]
user_defined_category = define_time_model_for_period(
data.get(EcalcYamlKeywords.user_defined_tag), target_period=self._target_period
)
cable_loss = None
max_usage_from_shore = None

if ConsumerUserDefinedCategoryType.POWER_FROM_SHORE in user_defined_category.values():
cable_loss = convert_expression(data.get(EcalcYamlKeywords.cable_loss))
max_usage_from_shore = convert_expression(data.get(EcalcYamlKeywords.max_usage_from_shore))

try:
generator_set_name = data.get(EcalcYamlKeywords.name)
return dto.GeneratorSet(
Expand All @@ -224,9 +234,9 @@ def from_yaml_to_dto(
regularity=regularity,
generator_set_model=generator_set_model,
consumers=consumers,
user_defined_category=define_time_model_for_period(
data.get(EcalcYamlKeywords.user_defined_tag), target_period=self._target_period
),
user_defined_category=user_defined_category,
cable_loss=cable_loss,
max_usage_from_shore=max_usage_from_shore,
)
except ValidationError as e:
raise DtoValidationError(data=data, validation_error=e) from e
Expand Down
Loading