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: add stream conditions to compressor v2 #194

Merged
merged 1 commit into from
Sep 26, 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
26 changes: 26 additions & 0 deletions src/libecalc/common/stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Literal

from libecalc.common.string_utils import to_camel_case
from libecalc.common.utils.rates import TimeSeriesFloat, TimeSeriesRate
from pydantic import BaseModel, Extra


class StreamCondition(BaseModel):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess usage here is in consumer system (only), but the entity should be called Stream, and it is the Stream that should be used in consumer system imo

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only consumer system now, but I'm hoping/expecting this to be used for all new components, also in models if we want to introduce TimeSeries there..

It's used in compressor (and when this is approved I will add it to pump) so when we add a pump and compressor v2 (outside system) it will be used there also

class Config:
extra = Extra.forbid
alias_generator = to_camel_case
allow_population_by_field_name = True

rate: TimeSeriesRate
pressure: TimeSeriesFloat
Copy link
Collaborator

Choose a reason for hiding this comment

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

liker denne, tenkte at her må vi spesifisere input og output stream. mye likt der, men trykke har jo naturlig nok forandra seg...og det kan andre ting også gjøre :) blir bra dette...og godt utgangspunkt for diskusjon :)

fluid_density: TimeSeriesFloat = None
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this for pump only? Or both compressor and pump? May easily lead to confusion if there is a difference. Not sure if it makes sense with a Fluid and FluidProperties for pump, as it is simpler..but may be that it is the same...or if we should call it Fluid wrt compressors since it is "mostly gas"...to be discussed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess we could have different types of streams, where pump expects a certain type, and compressor another. But I'm hoping we can find a common interface, where the stream itself can handle density etc. That means using streams as input though, so not yet.

But I agree, to be discussed.



class Stage(BaseModel):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Name easily confused with Compressor(Train)Stage, but somehow related (I guess). To be discussed. Not sure what is the name being used to define the stage/phase/step/ that the stream goes through...in the system...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

CompressorStage, ASVStage, ChokeStage. I was thinking the common name for all of these were stage, and that is what this represents, so stream is only one of the properties on a stage, might be the only one.. we'll see

class Config:
extra = Extra.forbid
alias_generator = to_camel_case
allow_population_by_field_name = True

name: Literal["inlet", "before_choke", "outlet"]
stream_condition: StreamCondition
Comment on lines +8 to +26
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me know if we have something similar already in models

27 changes: 27 additions & 0 deletions src/libecalc/common/tabular_time_series.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import itertools
from typing import Protocol, TypeVar

from libecalc.common.list_utils import transpose
from libecalc.common.utils.rates import TimeSeries
from pydantic import BaseModel
from typing_extensions import Self


Expand Down Expand Up @@ -52,5 +54,30 @@ def merge(cls, *objects_with_time_series: ObjectWithTimeSeries):
merged_object.__setattr__(key, merged_timesteps)
elif isinstance(value, TimeSeries):
merged_object.__setattr__(key, accumulated_value.merge(other_value))
elif isinstance(value, BaseModel):
merged_object.__setattr__(
key, cls.merge(*[obj.__getattribute__(key) for obj in objects_with_time_series])
)
elif (
isinstance(value, list)
and len(value) > 0
and (isinstance(value[0], TimeSeries) or isinstance(value[0], BaseModel))
):
list_attributes = [obj.__getattribute__(key) for obj in objects_with_time_series]
transposed_list_attributes = transpose(list_attributes)
merged_list_attributes = []
if isinstance(value[0], TimeSeries):
for time_series_to_merge in transposed_list_attributes:
first_time_series, *others_time_series = time_series_to_merge
merged_time_series = first_time_series
for other_time_series in others_time_series:
merged_time_series = merged_time_series.merge(other_time_series)
merged_list_attributes.append(merged_time_series)
elif isinstance(value[0], BaseModel):
merged_list_attributes = [
cls.merge(*objs_to_merge) for objs_to_merge in transposed_list_attributes
]

merged_object.__setattr__(key, merged_list_attributes)

return merged_object
52 changes: 44 additions & 8 deletions src/libecalc/core/consumers/compressor/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import numpy as np
from libecalc import dto
from libecalc.common.exceptions import ProgrammingError
from libecalc.common.stream import Stage, StreamCondition
from libecalc.common.temporal_model import TemporalModel
from libecalc.common.units import Unit
from libecalc.common.utils.rates import (
Expand Down Expand Up @@ -75,7 +76,7 @@ def evaluate(
evaluated_timesteps = []

# TODO: This is a false assumption and will be dealt with shortly (that the regularity is the same
# for all timesteps, and only taken for the first timestep)
# for all timesteps, and only taken for the first timestep). Not the first timestep, the first rate
evaluated_regularity = operational_settings.stream_day_rates[0].regularity
for timestep in operational_settings.timesteps:
compressor = self._temporal_model.get_model(timestep)
Expand Down Expand Up @@ -108,6 +109,24 @@ def evaluate(
if energy_usage.unit == Unit.STANDARD_CUBIC_METER_PER_DAY:
energy_usage = energy_usage.to_calendar_day() # provide fuel usage in calendar day, same as legacy consumer

# Creating a single input rate until we decide how to deal with multiple rates, multiple rates added because of
# multiple streams and pressures model, but we need to look at how streams are defined there.
total_requested_rate = TimeSeriesRate(
timesteps=operational_settings.timesteps,
values=list(np.sum([rate.values for rate in operational_settings.stream_day_rates], axis=0)),
unit=operational_settings.stream_day_rates[0].unit,
regularity=operational_settings.stream_day_rates[0].regularity,
rate_type=operational_settings.stream_day_rates[0].rate_type,
)

outlet_pressure_before_choke = TimeSeriesFloat(
values=aggregated_result.outlet_pressure_before_choking
if aggregated_result.outlet_pressure_before_choking
else [np.nan for _ in evaluated_timesteps],
timesteps=evaluated_timesteps,
unit=Unit.BARA,
)

component_result = core_results.CompressorResult(
timesteps=evaluated_timesteps,
power=TimeSeriesRate(
Expand All @@ -132,13 +151,30 @@ def evaluate(
timesteps=evaluated_timesteps,
unit=Unit.NONE,
),
outlet_pressure_before_choking=TimeSeriesFloat(
values=aggregated_result.outlet_pressure_before_choking
if aggregated_result.outlet_pressure_before_choking
else [np.nan for _ in evaluated_timesteps],
timesteps=evaluated_timesteps,
unit=Unit.BARA,
),
outlet_pressure_before_choking=outlet_pressure_before_choke,
stages=[
Stage(
name="inlet",
stream_condition=StreamCondition(
rate=total_requested_rate,
pressure=operational_settings.inlet_pressure,
),
),
Stage(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should be easy to extend this list with more stages from models.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I like the start to more structured add the different stage/phases etc of the stream that you are adding though :)

name="before_choke",
stream_condition=StreamCondition(
rate=total_requested_rate,
pressure=outlet_pressure_before_choke,
),
),
Stage(
name="outlet",
stream_condition=StreamCondition(
rate=total_requested_rate,
pressure=operational_settings.outlet_pressure,
),
),
],
)

return EcalcModelResult(
Expand Down
2 changes: 2 additions & 0 deletions src/libecalc/core/result/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from datetime import datetime
from typing import Any, Dict, List, Optional, Union

from libecalc.common.stream import Stage
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should go to core (now) or domain (later?) project package though, imo. But we will need a representation in the dto/application layer as well wrt input/output I guess...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, core/domain. Hopefully dto will be allowed to use it from core. It isn't all that clear to me anymore why we need a dto layer..We have a yaml layer, and can convert to something else there, why do we need a layer between yaml and core? I'm sure there is a reason, and I'm thinking it has something to do with evaluated vs not evaluated expressions..

It was introduced to allow changes, but in v2 we can make changes in core. I dunno

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's discuss .)

from libecalc.common.tabular_time_series import TabularTimeSeriesUtils
from libecalc.common.utils.rates import (
TimeSeriesBoolean,
Expand Down Expand Up @@ -62,6 +63,7 @@ class CompressorResult(GenericComponentResult):
recirculation_loss: TimeSeriesRate
rate_exceeds_maximum: TimeSeriesBoolean
outlet_pressure_before_choking: TimeSeriesFloat
stages: List[Stage] = None # Optional because only in v2

def get_subset(self, indices: List[int]) -> Self:
return self.__class__(
Expand Down
3 changes: 3 additions & 0 deletions src/libecalc/dto/result/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from libecalc.common.component_info.component_level import ComponentLevel
from libecalc.common.logger import logger
from libecalc.common.stream import Stage
from libecalc.common.time_utils import Frequency
from libecalc.common.units import Unit
from libecalc.common.utils.rates import (
Expand Down Expand Up @@ -117,6 +118,8 @@ class CompressorResult(EquipmentResultBase):
rate_exceeds_maximum: TimeSeriesBoolean
outlet_pressure_before_choking: TimeSeriesFloat

stages: List[Stage] = None # Optional because only in v2


class DirectEmitterResult(EquipmentResultBase):
componentType: Literal[ComponentType.DIRECT_EMITTER]
Expand Down
5 changes: 2 additions & 3 deletions src/libecalc/input/mappers/component_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ def from_yaml_to_dto(

if EcalcYamlKeywords.type in data:
if data[EcalcYamlKeywords.type] == ComponentType.COMPRESSOR_SYSTEM_V2:
compressor_system_yaml = YamlCompressorSystem(**data)
try:
compressor_system_yaml = YamlCompressorSystem(**data)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Improves error message when the yaml is incorrect

return compressor_system_yaml.to_dto(
consumes=consumes,
regularity=regularity,
Expand All @@ -120,9 +120,8 @@ def from_yaml_to_dto(
except ValidationError as e:
raise DtoValidationError(data=data, validation_error=e) from e
if data[EcalcYamlKeywords.type] == ComponentType.PUMP_SYSTEM_V2:
pump_system_yaml = YamlPumpSystem(**data)

try:
pump_system_yaml = YamlPumpSystem(**data)
return pump_system_yaml.to_dto(
consumes=consumes,
regularity=regularity,
Expand Down
3 changes: 1 addition & 2 deletions src/libecalc/input/validation_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,8 @@ def __init__(
error_key: Optional[str] = None,
dump_flow_style: Optional[DumpFlowStyle] = None,
):
self.message = message
super().__init__(message)

self.message = message
extended_message = "\nError in object\n"

if data is not None:
Expand Down
Loading