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

wip #317

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft

wip #317

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
31 changes: 14 additions & 17 deletions src/libecalc/common/priority_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,29 @@

from libecalc.common.priorities import PriorityID

TResult = TypeVar("TResult")

ComponentID = str


@dataclass
class PriorityOptimizerResult(Generic[TResult]):
priority_used: PriorityID
priority_results: List[typing.Any] # TODO: typing. This is the consumer results merged based on priorities used
class EvaluatorResult(typing.Protocol):
id: ComponentID
is_valid: bool


TResult = TypeVar("TResult", bound=EvaluatorResult)


@dataclass
class EvaluatorResult(Generic[TResult]):
id: ComponentID
result: TResult
is_valid: bool
class PriorityOptimizerResult(Generic[TResult]):
priority_used: PriorityID
priority_results: List[TResult]


class PriorityOptimizer(Generic[TResult]):
class PriorityOptimizer:
def optimize(
self,
priorities: List[PriorityID],
evaluator: typing.Callable[[PriorityID], List[EvaluatorResult[TResult]]],
) -> PriorityOptimizerResult:
evaluator: typing.Callable[[PriorityID], List[TResult]],
) -> PriorityOptimizerResult[TResult]:
"""
Given a list of priorities, evaluate each priority using the evaluator. If the result of an evaluation is valid
the priority is selected, if invalid try the next priority.
Expand All @@ -52,7 +51,7 @@ def optimize(
for priority in priorities:
evaluator_results = evaluator(priority)
for evaluator_result in evaluator_results:
priority_results[priority][evaluator_result.id] = evaluator_result.result
priority_results[priority][evaluator_result.id] = evaluator_result

# Check if consumers are valid for this priority, should be valid for all consumers
all_evaluator_results_valid = reduce(
Expand All @@ -65,7 +64,5 @@ def optimize(
break
return PriorityOptimizerResult(
priority_used=priority_used,
priority_results=[
ecalc_model_result.component_result for ecalc_model_result in priority_results[priority_used].values()
],
priority_results=list(priority_results[priority_used].values()),
)
2 changes: 1 addition & 1 deletion src/libecalc/core/consumers/base/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .component import BaseConsumer, BaseConsumerWithoutOperationalSettings
from .component import Consumer, ConsumerID
19 changes: 5 additions & 14 deletions src/libecalc/core/consumers/base/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,18 @@

from libecalc.common.stream_conditions import TimeSeriesStreamConditions
from libecalc.common.utils.rates import TimeSeriesFloat
from libecalc.core.result import EcalcModelResult
from libecalc.dto import VariablesMap
from libecalc.core.consumers.base.result import ConsumerResult


class BaseConsumer(ABC):
@abstractmethod
def evaluate(
self,
variables_map: VariablesMap,
temporal_operational_settings,
) -> EcalcModelResult:
...
ConsumerID = str


class BaseConsumerWithoutOperationalSettings(ABC):
id: str
class Consumer(ABC):
id: ConsumerID

@abstractmethod
def get_max_rate(self, inlet_stream: TimeSeriesStreamConditions, target_pressure: TimeSeriesFloat) -> List[float]:
...

@abstractmethod
def evaluate(self, **kwargs) -> EcalcModelResult:
def evaluate(self, **kwargs) -> ConsumerResult:
...
77 changes: 77 additions & 0 deletions src/libecalc/core/consumers/base/result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from abc import abstractmethod
from datetime import datetime
from functools import partial
from typing import List, Optional, Protocol

from orjson import orjson
from pydantic import BaseModel, Extra
from pydantic.json import custom_pydantic_encoder

from libecalc.common.string.string_utils import to_camel_case
from libecalc.core.consumers.base.component import ConsumerID
from libecalc.domain.stream_conditions import Rate, StreamConditions


def orjson_dumps(v, *, default, indent: bool = False):
options = orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_PASSTHROUGH_DATETIME

if indent:
options = options | orjson.OPT_INDENT_2

# orjson.dumps returns bytes, to match standard json.dumps we need to decode
# default is the pydantic json encoder
return orjson.dumps(v, default=default, option=options).decode("utf-8")


class EcalcBaseModel(BaseModel):
class Config:
extra = Extra.forbid
alias_generator = to_camel_case
allow_population_by_field_name = True
json_dumps = orjson_dumps
json_encoders = {
datetime: lambda v: v.strftime("%Y-%m-%dT%H:%M:%S"),
}
copy_on_model_validation = "deep"

def json(self, date_format: Optional[str] = None, **kwargs) -> str:
if date_format is None:
return super().json(**kwargs)

if kwargs.get("encoder") is None:
# Override datetime encoder if not already overridden, use user specified date_format_option
encoder = partial(
custom_pydantic_encoder,
{
datetime: lambda v: v.strftime(date_format),
},
)
else:
encoder = kwargs["encoder"]

return super().json(**kwargs, encoder=encoder) # Encoder becomes default, i.e. should handle unhandled types


class ModelResult(Protocol):
@abstractmethod
@property
def streams(self) -> List[StreamConditions]:
...


class ConsumerResult(EcalcBaseModel):
"""Base component for all results: Model, Installation, GenSet, Consumer System, Consumer, etc."""

id: ConsumerID
timestep: datetime
is_valid: bool

# We need both energy usage and power rate since we sometimes want both fuel and power usage.
energy_usage: Rate
power: Optional[Rate]
streams: List[StreamConditions]

@abstractmethod
@property
def models(self) -> List[ModelResult]:
...
103 changes: 20 additions & 83 deletions src/libecalc/core/consumers/compressor/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,17 @@

import numpy as np

from libecalc.common.stream_conditions import TimeSeriesStreamConditions
from libecalc.common.units import Unit
from libecalc.common.utils.rates import (
TimeSeriesBoolean,
TimeSeriesFloat,
TimeSeriesStreamDayRate,
)
from libecalc.core.consumers.base import BaseConsumerWithoutOperationalSettings
from libecalc.core.consumers.base import Consumer
from libecalc.core.consumers.compressor.result import CompressorResult
from libecalc.core.models.compressor import CompressorModel
from libecalc.core.result import EcalcModelResult
from libecalc.core.result import results as core_results
from libecalc.domain.stream_conditions import Pressure, StreamConditions
from libecalc.domain.stream_conditions import Pressure, Rate, StreamConditions
from libecalc.dto.core_specs.compressor.operational_settings import (
CompressorOperationalSettings,
)


class Compressor(BaseConsumerWithoutOperationalSettings):
class Compressor(Consumer):
def __init__(self, id: str, compressor_model: CompressorModel):
self.id = id
self._compressor_model = compressor_model
Expand All @@ -43,7 +36,7 @@ def get_max_rate(self, inlet_stream: StreamConditions, target_pressure: Pressure
def evaluate(
self,
streams: List[StreamConditions],
) -> EcalcModelResult:
) -> CompressorResult:
inlet_streams = streams[:-1]
outlet_stream = streams[-1]

Expand All @@ -59,79 +52,23 @@ def evaluate(

outlet_stream.rate = total_requested_inlet_stream.rate

energy_usage = TimeSeriesStreamDayRate(
values=model_result.energy_usage,
timesteps=[current_timestep],
unit=model_result.energy_usage_unit,
)

outlet_pressure_before_choke = TimeSeriesFloat(
values=model_result.outlet_pressure_before_choking
if model_result.outlet_pressure_before_choking
else [np.nan],
timesteps=[current_timestep],
unit=Unit.BARA,
)

component_result = core_results.CompressorResult(
timesteps=[current_timestep],
power=TimeSeriesStreamDayRate(
values=model_result.power,
timesteps=[current_timestep],
unit=model_result.power_unit,
).fill_nan(0.0),
energy_usage=energy_usage.fill_nan(0.0),
is_valid=TimeSeriesBoolean(values=model_result.is_valid, timesteps=[current_timestep], unit=Unit.NONE),
return CompressorResult(
id=self.id,
recirculation_loss=TimeSeriesStreamDayRate(
values=model_result.recirculation_loss,
timesteps=[current_timestep],
unit=Unit.MEGA_WATT,
),
rate_exceeds_maximum=TimeSeriesBoolean(
values=model_result.rate_exceeds_maximum,
timesteps=[current_timestep],
unit=Unit.NONE,
timestep=current_timestep,
power=Rate(
value=model_result.power[0],
unit=model_result.power_unit,
),
outlet_pressure_before_choking=outlet_pressure_before_choke,
energy_usage=Rate(value=model_result.energy_usage[0], unit=model_result.energy_usage_unit),
is_valid=model_result.is_valid[0],
recirculation_loss=Rate(value=model_result.recirculation_loss[0], unit=Unit.MEGA_WATT),
rate_exceeds_maximum=model_result.rate_exceeds_maximum[0],
streams=[
TimeSeriesStreamConditions.from_stream_condition(total_requested_inlet_stream),
*[
TimeSeriesStreamConditions.from_stream_condition(inlet_stream_conditions)
for inlet_stream_conditions in inlet_streams
],
TimeSeriesStreamConditions.from_stream_condition(outlet_stream),
],
)

return EcalcModelResult(
component_result=component_result,
sub_components=[],
models=[
core_results.CompressorModelResult(
name="N/A", # No context available to populate model name
timesteps=[current_timestep],
is_valid=TimeSeriesBoolean(
timesteps=[current_timestep],
values=model_result.is_valid,
unit=Unit.NONE,
),
power=TimeSeriesStreamDayRate(
timesteps=[current_timestep],
values=model_result.power,
unit=model_result.power_unit,
)
if model_result.power is not None
else None,
energy_usage=TimeSeriesStreamDayRate(
timesteps=[current_timestep],
values=model_result.energy_usage,
unit=model_result.energy_usage_unit,
),
energy_usage_unit=model_result.energy_usage_unit,
rate_sm3_day=model_result.rate_sm3_day,
stage_results=model_result.stage_results,
failure_status=model_result.failure_status,
)
total_requested_inlet_stream,
*inlet_streams,
outlet_stream.copy(
{"pressure": Pressure(value=model_result.outlet_pressure_before_choking[0], unit=Unit.BARA)}
),
outlet_stream,
],
)
11 changes: 11 additions & 0 deletions src/libecalc/core/consumers/compressor/result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from libecalc.core.consumers.base.result import ConsumerResult
from libecalc.domain.stream_conditions import Rate


class CompressorResult(ConsumerResult):
recirculation_loss: Rate
rate_exceeds_maximum: bool # Seems very specific, should we replace this and is_valid with failure flags?

@property
def models(self):
return []
13 changes: 3 additions & 10 deletions src/libecalc/core/consumers/consumer_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@

import networkx as nx

from libecalc.common.priority_optimizer import EvaluatorResult
from libecalc.common.utils.rates import (
TimeSeriesInt,
)
from libecalc.core.consumers.base.result import ConsumerResult
from libecalc.core.consumers.compressor import Compressor
from libecalc.core.consumers.pump import Pump
from libecalc.core.result import ComponentResult, ConsumerSystemResult, EcalcModelResult
Expand Down Expand Up @@ -96,7 +96,7 @@ def _get_stream_conditions_adjusted_for_crossover(

return adjusted_stream_conditions

def evaluate_consumers(self, system_stream_conditions: Dict[str, List[StreamConditions]]) -> List[EvaluatorResult]:
def evaluate_consumers(self, system_stream_conditions: Dict[str, List[StreamConditions]]) -> List[ConsumerResult]:
"""
Function to evaluate the consumers in the system given stream conditions for a single point in time

Expand All @@ -112,14 +112,7 @@ def evaluate_consumers(self, system_stream_conditions: Dict[str, List[StreamCond
consumer_results_for_priority = [
consumer.evaluate(adjusted_system_stream_conditions[consumer.id]) for consumer in self._consumers
]
return [
EvaluatorResult(
id=consumer_result_for_priority.component_result.id,
result=consumer_result_for_priority,
is_valid=consumer_result_for_priority.component_result.is_valid.values[0],
)
for consumer_result_for_priority in consumer_results_for_priority
]
return consumer_results_for_priority

@staticmethod
def get_system_result(
Expand Down
12 changes: 11 additions & 1 deletion src/libecalc/core/consumers/legacy_consumer/component.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import itertools
import math
from abc import ABC, abstractmethod
from collections import defaultdict
from datetime import datetime
from typing import DefaultDict, Iterable, List, Union
Expand All @@ -19,7 +20,6 @@
TimeSeriesInt,
TimeSeriesStreamDayRate,
)
from libecalc.core.consumers.base import BaseConsumer
from libecalc.core.consumers.legacy_consumer.consumer_function import (
ConsumerFunctionResult,
)
Expand Down Expand Up @@ -61,6 +61,16 @@ def get_operational_settings_used_from_consumer_result(
ConsumerResult = Union[ConsumerSystemResult, PumpResult, CompressorResult]


class BaseConsumer(ABC):
@abstractmethod
def evaluate(
self,
variables_map: VariablesMap,
temporal_operational_settings,
) -> EcalcModelResult:
...


class Consumer(BaseConsumer):
def __init__(
self,
Expand Down
1 change: 1 addition & 0 deletions src/libecalc/core/consumers/pump/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .component import Pump
from .result import PumpResult
Loading
Loading