Skip to content

Commit

Permalink
feat: new PfS columns (#429)
Browse files Browse the repository at this point in the history
* feat: new PfS columns

ECALC-365
  • Loading branch information
frodehk authored and equinor-schen committed Aug 23, 2024
1 parent 679d24e commit 2e8da0a
Show file tree
Hide file tree
Showing 10 changed files with 411 additions and 6 deletions.
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
1 change: 1 addition & 0 deletions src/libecalc/fixtures/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
)
from .cases.consumer_with_time_slots_models import * # noqa: F403
from .cases.ltp_export import ltp_export_yaml
from .cases.ltp_export.ltp_power_from_shore_yaml import ltp_pfs_yaml_factory
from .cases.minimal import * # noqa: F403
from .compressor_process_simulations.compressor_process_simulations import * # noqa: F403
from .conftest import (
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,98 @@
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

0 comments on commit 2e8da0a

Please sign in to comment.