Skip to content
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
7 changes: 5 additions & 2 deletions flixopt/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import functools
import logging
import warnings
from typing import TYPE_CHECKING, Literal
Expand Down Expand Up @@ -1102,7 +1103,7 @@ def _absolute_charge_state_bounds(self) -> tuple[xr.DataArray, xr.DataArray]:
relative_upper_bound * cap,
)

@property
@functools.cached_property
def _relative_charge_state_bounds(self) -> tuple[xr.DataArray, xr.DataArray]:
"""
Get relative charge state bounds with final timestep values.
Expand Down Expand Up @@ -1152,7 +1153,9 @@ def _relative_charge_state_bounds(self) -> tuple[xr.DataArray, xr.DataArray]:
# Original is scalar - broadcast to full time range (constant value)
max_bounds = rel_max.expand_dims(time=timesteps_extra)

return min_bounds, max_bounds
# Ensure both bounds have matching dimensions (broadcast once here,
# so downstream code doesn't need to handle dimension mismatches)
return xr.broadcast(min_bounds, max_bounds)

@property
def _investment(self) -> InvestmentModel | None:
Expand Down
7 changes: 5 additions & 2 deletions flixopt/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import functools
import logging
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -866,11 +867,13 @@ def _create_bounds_for_load_factor(self):
short_name='load_factor_min',
)

@property
@functools.cached_property
def relative_flow_rate_bounds(self) -> tuple[xr.DataArray, xr.DataArray]:
if self.element.fixed_relative_profile is not None:
return self.element.fixed_relative_profile, self.element.fixed_relative_profile
return self.element.relative_minimum, self.element.relative_maximum
# Ensure both bounds have matching dimensions (broadcast once here,
# so downstream code doesn't need to handle dimension mismatches)
return xr.broadcast(self.element.relative_minimum, self.element.relative_maximum)

@property
def absolute_flow_rate_bounds(self) -> tuple[xr.DataArray, xr.DataArray]:
Expand Down
25 changes: 23 additions & 2 deletions flixopt/modeling.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ def _scalar_safe_reduce(data: xr.DataArray | Any, dim: str, method: str = 'mean'
return data


def _xr_allclose(a: xr.DataArray, b: xr.DataArray, rtol: float = 1e-5, atol: float = 1e-8) -> bool:
"""Check if two DataArrays are element-wise equal within tolerance.

Args:
a: First DataArray
b: Second DataArray
rtol: Relative tolerance (default matches np.allclose)
atol: Absolute tolerance (default matches np.allclose)

Returns:
True if all elements are close (including matching NaN positions)
"""
# Fast path: same dims and shape - use numpy directly
if a.dims == b.dims and a.shape == b.shape:
return np.allclose(a.values, b.values, rtol=rtol, atol=atol, equal_nan=True)

# Slow path: broadcast to common shape, then use numpy
a_bc, b_bc = xr.broadcast(a, b)
return np.allclose(a_bc.values, b_bc.values, rtol=rtol, atol=atol, equal_nan=True)


class ModelingUtilitiesAbstract:
"""Utility functions for modeling - leveraging xarray for temporal data"""

Expand Down Expand Up @@ -546,7 +567,7 @@ def bounds_with_state(
lower_bound, upper_bound = bounds
name = name or f'{variable.name}'

if np.allclose(lower_bound, upper_bound, atol=1e-10, equal_nan=True):
if _xr_allclose(lower_bound, upper_bound):
fix_constraint = model.add_constraints(variable == state * upper_bound, name=f'{name}|fix')
return [fix_constraint]

Expand Down Expand Up @@ -588,7 +609,7 @@ def scaled_bounds(
rel_lower, rel_upper = relative_bounds
name = name or f'{variable.name}'

if np.allclose(rel_lower, rel_upper, atol=1e-10, equal_nan=True):
if _xr_allclose(rel_lower, rel_upper):
return [model.add_constraints(variable == scaling_variable * rel_lower, name=f'{name}|fixed')]

upper_constraint = model.add_constraints(variable <= scaling_variable * rel_upper, name=f'{name}|ub')
Expand Down