Skip to content

Commit

Permalink
feat: support name for crossover streams (#236)
Browse files Browse the repository at this point in the history
Allow handling of crossover streams in multiple streams.
  • Loading branch information
jsolaas authored Oct 18, 2023
1 parent 2981f2c commit c801f3f
Show file tree
Hide file tree
Showing 12 changed files with 260 additions and 53 deletions.
1 change: 1 addition & 0 deletions src/libecalc/common/string_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ def to_camel_case(string: str) -> str:
"""
string_split = string.replace("__", "_").split("_")
string_split = [word for word in string_split if len(word) > 0] # Allow names such as 'from_'
return string_split[0] + "".join(word[0].upper() + word[1:] for word in string_split[1:])
52 changes: 28 additions & 24 deletions src/libecalc/core/consumers/consumer_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from collections import defaultdict
from datetime import datetime
from functools import reduce
from typing import Dict, List, Protocol, Tuple, TypeVar, Union
from typing import Dict, List, Optional, Protocol, Tuple, TypeVar, Union

import networkx as nx
import numpy as np
Expand Down Expand Up @@ -47,8 +47,14 @@ def get_subset_for_timestep(self, current_timestep: datetime) -> Self:
...


class Crossover(Protocol):
stream_name: Optional[str]
from_component_id: str
to_component_id: str


class SystemComponentConditions(Protocol):
crossover: List[int]
crossover: List[Crossover]


class SystemOperationalSettings(Protocol):
Expand Down Expand Up @@ -95,31 +101,28 @@ def _get_operational_settings_adjusted_for_crossover(
)
adjusted_operational_settings = []

crossover_streams_map: Dict[int, List[Stream]] = {
consumer_index: [] for consumer_index in range(len(self._consumers))
crossover_streams_map: Dict[str, List[Stream]] = {consumer.id: [] for consumer in self._consumers}
crossover_definitions_map: Dict[str, Crossover] = {
crossover_stream.from_component_id: crossover_stream
for crossover_stream in self._component_conditions.crossover
}

# Converting from index 1 to index 0.
crossover = [crossover_flow_to_index - 1 for crossover_flow_to_index in self._component_conditions.crossover]

for consumer in sorted_consumers:
consumer_index = self._consumers.index(consumer)
consumer_operational_settings = operational_setting.get_consumer_operational_settings(
consumer_index, timesteps=variables_map.time_vector
)
inlet_streams = list(consumer_operational_settings.inlet_streams)

crossover_to = crossover[consumer_index]
has_crossover_out = crossover_to >= 0
# Currently only implemented for one crossover out per consumer
crossover_stream_definition = crossover_definitions_map.get(consumer.id)
has_crossover_out = crossover_stream_definition is not None
if has_crossover_out:
consumer = self._consumers[consumer_index]
max_rate = consumer.get_max_rate(consumer_operational_settings)

crossover_stream, inlet_streams = ConsumerSystem._get_crossover_streams(max_rate, inlet_streams)
crossover_streams_map[crossover_stream_definition.to_component_id].append(crossover_stream)

crossover_streams_map[crossover_to].append(crossover_stream)

consumer_operational_settings.inlet_streams = [*inlet_streams, *crossover_streams_map[consumer_index]]
consumer_operational_settings.inlet_streams = [*inlet_streams, *crossover_streams_map[consumer.id]]
adjusted_operational_settings.append(consumer_operational_settings)

# This is a hack to return operational settings in the original order of the consumers. This way we can
Expand Down Expand Up @@ -256,29 +259,30 @@ def collect_consumer_results(
return list(consumer_results.values())

@staticmethod
def _topologically_sort_consumers_by_crossover(crossover: List[int], consumers: List[Consumer]) -> List[Consumer]:
def _topologically_sort_consumers_by_crossover(
crossover: List[Crossover], consumers: List[Consumer]
) -> List[Consumer]:
"""Topological sort of the graph created by crossover. This makes it possible for us to evaluate each
consumer with the correct rate directly.
Parameters
----------
crossover: list of indexes in consumers for crossover. 0 means no cross-over, 1 refers to first consumer etc.
crossover: list of crossover stream definitions
consumers
-------
List of topological sorted consumers
"""
zero_indexed_crossover = [i - 1 for i in crossover] # Use zero-index
graph = nx.DiGraph()
for consumer_index in range(len(consumers)):
graph.add_node(consumer_index)
for consumer in consumers:
graph.add_node(consumer.id)

for crossover_stream in crossover:
graph.add_edge(crossover_stream.from_component_id, crossover_stream.to_component_id)

for this, other in enumerate(zero_indexed_crossover):
# No crossover (0) => -1 in zero-indexed crossover
if other >= 0:
graph.add_edge(this, other)
consumers_by_id = {consumer.id: consumer for consumer in consumers}

return [consumers[consumer_index] for consumer_index in nx.topological_sort(graph)]
return [consumers_by_id[consumer_id] for consumer_id in nx.topological_sort(graph)]

@staticmethod
def _get_crossover_streams(max_rate: List[float], inlet_streams: List[Stream]) -> Tuple[Stream, List[Stream]]:
Expand Down
11 changes: 10 additions & 1 deletion src/libecalc/dto/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,17 @@ class PumpSystemOperationalSetting(EcalcBaseModel):
outlet_pressures: List[Expression]


class Crossover(EcalcBaseModel):
class Config:
allow_population_by_field_name = True

stream_name: Optional[str] = Field(None)
from_component_id: str
to_component_id: str


class SystemComponentConditions(EcalcBaseModel):
crossover: List[int]
crossover: List[Crossover]


class CompressorSystem(BaseConsumer):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import pytest
from libecalc import dto
from libecalc.common.string_utils import generate_id
from libecalc.dto import (
CompressorSystemCompressor,
CompressorSystemConsumerFunction,
Expand All @@ -17,7 +18,7 @@
FuelTypeUserDefinedCategoryType,
InstallationUserDefinedCategoryType,
)
from libecalc.dto.components import SystemComponentConditions
from libecalc.dto.components import Crossover, SystemComponentConditions
from libecalc.dto.types import ConsumptionType, EnergyUsageType
from libecalc.expression import Expression
from libecalc.fixtures.case_types import DTOCase
Expand Down Expand Up @@ -196,7 +197,10 @@
consumes=ConsumptionType.FUEL,
fuel=fuel,
component_conditions=SystemComponentConditions(
crossover=[0, 1, 1],
crossover=[
Crossover(from_component_id=generate_id("compressor2"), to_component_id=generate_id("compressor1")),
Crossover(from_component_id=generate_id("compressor3"), to_component_id=generate_id("compressor1")),
],
),
operational_settings={
datetime(2022, 1, 1, 0, 0): [
Expand Down Expand Up @@ -231,7 +235,10 @@
consumes=ConsumptionType.FUEL,
fuel=fuel,
component_conditions=SystemComponentConditions(
crossover=[0, 1, 1],
crossover=[
Crossover(from_component_id=generate_id("compressor2"), to_component_id=generate_id("compressor1")),
Crossover(from_component_id=generate_id("compressor3"), to_component_id=generate_id("compressor1")),
],
),
operational_settings={
datetime(2022, 1, 1, 0, 0): [
Expand Down Expand Up @@ -283,7 +290,10 @@
consumes=ConsumptionType.FUEL,
fuel=fuel,
component_conditions=SystemComponentConditions(
crossover=[0, 1, 1],
crossover=[
Crossover(from_component_id=generate_id("compressor2"), to_component_id=generate_id("compressor1")),
Crossover(from_component_id=generate_id("compressor3"), to_component_id=generate_id("compressor1")),
],
),
operational_settings={
datetime(2022, 1, 1, 0, 0): [
Expand Down Expand Up @@ -378,7 +388,10 @@
regularity=regularity,
consumes=dto.types.ConsumptionType.ELECTRICITY,
component_conditions=SystemComponentConditions(
crossover=[0, 1, 1],
crossover=[
Crossover(from_component_id=generate_id("pump2"), to_component_id=generate_id("pump1")),
Crossover(from_component_id=generate_id("pump3"), to_component_id=generate_id("pump1")),
],
),
operational_settings={
datetime(2022, 1, 1, 0, 0): [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,11 @@ INSTALLATIONS:
- NAME: pump3
ENERGY_USAGE_MODEL: pump_single_speed
COMPONENT_CONDITIONS:
CROSSOVER: [ 0, 1, 1 ]
CROSSOVER:
- FROM: pump2
TO: pump1
- FROM: pump3
TO: pump1
OPERATIONAL_SETTINGS:
- RATES: [ 4000000, 5000000, 6000000 ]
INLET_PRESSURE: 50
Expand Down Expand Up @@ -126,7 +130,11 @@ INSTALLATIONS:
- NAME: compressor3
ENERGY_USAGE_MODEL: compressor_sampled_1d
COMPONENT_CONDITIONS:
CROSSOVER: [ 0, 1, 1 ]
CROSSOVER:
- FROM: compressor2
TO: compressor1
- FROM: compressor3
TO: compressor1
OPERATIONAL_SETTINGS:
- RATES: [ 1000000, 6000000, 6000000 ]
INLET_PRESSURE: 50
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from libecalc import dto
from libecalc.common.time_utils import Period, define_time_model_for_period
from libecalc.dto.base import ComponentType
from libecalc.dto.components import SystemComponentConditions
from libecalc.dto.components import Crossover, SystemComponentConditions
from libecalc.dto.types import ConsumptionType
from libecalc.expression import Expression
from libecalc.expression.expression import ExpressionType
Expand Down Expand Up @@ -140,15 +140,24 @@ def to_dto(
for compressor in self.consumers
]

compressor_name_to_id_map = {compressor.name: compressor.id for compressor in compressors}

if self.component_conditions is not None:
component_conditions = SystemComponentConditions(
crossover=self.component_conditions.crossover
crossover=[
Crossover(
from_component_id=compressor_name_to_id_map[crossover_stream.from_],
to_component_id=compressor_name_to_id_map[crossover_stream.to],
stream_name=crossover_stream.name,
)
for crossover_stream in self.component_conditions.crossover
]
if self.component_conditions.crossover is not None
else [0] * number_of_compressors,
else [],
)
else:
component_conditions = SystemComponentConditions(
crossover=[0] * number_of_compressors,
crossover=[],
)

return dto.components.CompressorSystem(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from libecalc import dto
from libecalc.common.time_utils import Period, define_time_model_for_period
from libecalc.dto.base import ComponentType
from libecalc.dto.components import SystemComponentConditions
from libecalc.dto.components import Crossover, SystemComponentConditions
from libecalc.dto.types import ConsumptionType
from libecalc.expression import Expression
from libecalc.expression.expression import ExpressionType
Expand Down Expand Up @@ -170,15 +170,24 @@ def to_dto(
for pump in self.consumers
]

pump_name_to_id_map = {pump.name: pump.id for pump in pumps}

if self.component_conditions is not None:
component_conditions = SystemComponentConditions(
crossover=self.component_conditions.crossover
crossover=[
Crossover(
from_component_id=pump_name_to_id_map[crossover_stream.from_],
to_component_id=pump_name_to_id_map[crossover_stream.to],
stream_name=crossover_stream.name,
)
for crossover_stream in self.component_conditions.crossover
]
if self.component_conditions.crossover is not None
else [0] * number_of_pumps,
else [],
)
else:
component_conditions = SystemComponentConditions(
crossover=[0] * number_of_pumps,
crossover=[],
)

return dto.components.PumpSystem(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
from typing import List, Optional

from libecalc.common.string_utils import get_duplicates
from libecalc.input.yaml_types import YamlBase
from pydantic import Field
from pydantic import Field, validator


class YamlCrossover(YamlBase):
class Config:
allow_population_by_field_name = True

name: str = Field(
None,
title="NAME",
description="The name of the stream. "
"Can be used to identify the crossover stream in multiple streams compressor train",
)
from_: str = Field(
...,
title="FROM",
description="Target component for crossover",
alias="FROM",
)
to: str = Field(
...,
title="TO",
description="Target component for crossover",
)


class YamlSystemComponentConditions(YamlBase):
crossover: Optional[List[int]] = Field(
crossover: Optional[List[YamlCrossover]] = Field(
None,
title="Crossover",
description=(
Expand All @@ -23,9 +47,29 @@ class YamlSystemComponentConditions(YamlBase):
"Example 1:\n"
"Two consumers where there is a cross-over such that if the rate for the first consumer exceeds its capacity,"
" the excess rate will be processed by the second consumer. The second consumer can not cross-over to anyone.\n"
"CROSSOVER: [2, 0]\n"
"CROSSOVER: \n"
" - FROM: consumer1 \n"
" TO: consumer2 \n"
"Example 2:\n"
"The first and second consumers may both send exceeding rate to the third consumer if their capacity is exceeded.\n"
"CROSSOVER: [3,3,0]"
"CROSSOVER: \n"
" - FROM: consumer1 \n"
" TO: consumer3 \n"
" - FROM: consumer2 \n"
" TO: consumer3 \n"
),
)

@validator("crossover")
def ensure_one_crossover_out(cls, crossover: Optional[List[YamlCrossover]]):
if crossover is None:
return None
crossover_out = [c.from_ for c in crossover]
unique_crossover_out = set(crossover_out)
if len(unique_crossover_out) != len(crossover_out):
raise ValueError(
f"Only one crossover per consumer is currently supported. Component(s) with several crossover streams "
f"are {', '.join(sorted(get_duplicates(crossover_out)))}"
)

return crossover
8 changes: 4 additions & 4 deletions src/tests/libecalc/common/test_string_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ def test_multiple_strings(self):
("m_c", "mC"),
("m_C", "mC"),
("M_C", "MC"),
# ("_C", "C"),
# ("_c", "C"),
# ("m_", "m"),
# ("M_", "M"),
("_C", "C"),
("_c", "c"),
("m_", "m"),
("M_", "M"),
("my_cAmeLCase", "myCAmeLCase"),
]

Expand Down
Loading

0 comments on commit c801f3f

Please sign in to comment.