-
Notifications
You must be signed in to change notification settings - Fork 10
feat(process): OutletFluidNotAchievableError and SpeedSolver EOS failure handling #1543
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
base: main
Are you sure you want to change the base?
Changes from all commits
62eeffa
3cf7ffd
24ed584
4cfc595
cbf60bf
2542e7b
754df3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,11 +2,24 @@ | |
| from collections.abc import Callable | ||
|
|
||
| from libecalc.process.fluid_stream.fluid_stream import FluidStream | ||
| from libecalc.process.process_pipeline.process_error import RateTooHighError, RateTooLowError | ||
| from libecalc.process.process_pipeline.process_error import ( | ||
| OutletFluidNotAchievableError, | ||
| RateTooHighError, | ||
| RateTooLowError, | ||
| ) | ||
| from libecalc.process.process_solver.boundary import Boundary | ||
| from libecalc.process.process_solver.configuration import SpeedConfiguration | ||
| from libecalc.process.process_solver.search_strategies import RootFindingStrategy, SearchStrategy | ||
| from libecalc.process.process_solver.search_strategies import ( | ||
| ACCEPT_AND_GO_HIGHER, | ||
| ACCEPT_AND_GO_LOWER, | ||
| REJECT_AND_GO_HIGHER, | ||
| REJECT_AND_GO_LOWER, | ||
| BinarySearchResult, | ||
| RootFindingStrategy, | ||
| SearchStrategy, | ||
| ) | ||
| from libecalc.process.process_solver.solver import ( | ||
| OutletFluidNotAchievableEvent, | ||
| OutsideCapacityEvent, | ||
| Solution, | ||
| Solver, | ||
|
|
@@ -61,6 +74,12 @@ def get_outlet_stream(speed: float) -> FluidStream: | |
| source_id=e.process_unit_id, | ||
| ), | ||
| ) | ||
| except OutletFluidNotAchievableError: | ||
| result = self._search_highest_eos_feasible_speed(get_outlet_stream) | ||
| if isinstance(result, Solution): | ||
| return result | ||
| max_speed_configuration = result | ||
| maximum_speed_outlet_stream = get_outlet_stream(speed=result.speed) | ||
|
|
||
| if maximum_speed_outlet_stream.pressure_bara < self._target_pressure: | ||
| return Solution( | ||
|
|
@@ -95,6 +114,12 @@ def bool_speed_func(x: float): | |
| ) | ||
| minimum_speed_configuration = SpeedConfiguration(speed=minimum_speed_within_capacity) | ||
| minimum_speed_outlet_stream = func(minimum_speed_configuration) | ||
| except OutletFluidNotAchievableError: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this method is getting long, would it make sense to split it into (reusable) steps/phases - to easier understand/read each step?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes! |
||
| result = self._search_lowest_eos_feasible_speed(get_outlet_stream) | ||
| if isinstance(result, Solution): | ||
| return result | ||
| minimum_speed_configuration = result | ||
| minimum_speed_outlet_stream = get_outlet_stream(speed=result.speed) | ||
|
|
||
| if minimum_speed_outlet_stream.pressure_bara > self._target_pressure: | ||
| # Solution 2, target pressure is too low | ||
|
|
@@ -121,8 +146,85 @@ def root_speed_func(x: float) -> float: | |
| out = get_outlet_stream(speed=x) | ||
| return out.pressure_bara - self._target_pressure | ||
|
|
||
| speed = self._root_finding_strategy.find_root( | ||
| boundary=Boundary(min=minimum_speed_configuration.speed, max=self._boundary.max), | ||
| func=root_speed_func, | ||
| ) | ||
| try: | ||
| speed = self._root_finding_strategy.find_root( | ||
| boundary=Boundary(min=minimum_speed_configuration.speed, max=max_speed_configuration.speed), | ||
| func=root_speed_func, | ||
| ) | ||
| except OutletFluidNotAchievableError as e: | ||
| return self._fluid_not_achievable_solution( | ||
| e, SpeedConfiguration(speed=e.unachievable_operating_point.speed) | ||
| ) | ||
|
|
||
| return Solution(success=True, configuration=SpeedConfiguration(speed=speed)) | ||
|
|
||
| def _fluid_not_achievable_solution( | ||
| self, | ||
| e: OutletFluidNotAchievableError, | ||
| configuration: SpeedConfiguration, | ||
| ) -> Solution[SpeedConfiguration]: | ||
| logger.warning(f"Outlet fluid not achievable at speed {configuration.speed}", exc_info=e) | ||
| return Solution( | ||
| success=False, | ||
| configuration=configuration, | ||
| failure_event=OutletFluidNotAchievableEvent( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This new failure event still needs a caller-side short-circuit. |
||
| status=SolverFailureStatus.OUTLET_FLUID_NOT_ACHIEVABLE, | ||
| source_id=e.process_unit_id, | ||
| unachievable_operating_point=e.unachievable_operating_point, | ||
| ), | ||
| ) | ||
|
|
||
| def _search_highest_eos_feasible_speed( | ||
| self, | ||
| get_outlet_stream: Callable[[float], FluidStream], | ||
| ) -> SpeedConfiguration | Solution[SpeedConfiguration]: | ||
| """Binary-search downward for the highest speed where the EOS produces a valid outlet. | ||
|
|
||
| Pre-checks that boundary.min is feasible; if not, returns a failed Solution | ||
| (the search would otherwise exhaust iterations without converging). | ||
| """ | ||
| logger.debug( | ||
| "Outlet fluid not achievable at maximum speed %.1f — searching for highest feasible speed.", | ||
| self._boundary.max, | ||
| ) | ||
| try: | ||
| get_outlet_stream(self._boundary.min) | ||
| except OutletFluidNotAchievableError as e: | ||
| return self._fluid_not_achievable_solution(e, SpeedConfiguration(speed=self._boundary.min)) | ||
|
|
||
| def feasible(x: float) -> BinarySearchResult: | ||
| try: | ||
| get_outlet_stream(x) | ||
| return ACCEPT_AND_GO_HIGHER | ||
| except (OutletFluidNotAchievableError, RateTooHighError, RateTooLowError): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This search can still leak a capacity error when the lower end of the speed range is invalid for capacity while the upper end is invalid for EOS. For example, |
||
| return REJECT_AND_GO_LOWER | ||
|
|
||
| speed = self._search_strategy.search(boundary=self._boundary, func=feasible) | ||
| return SpeedConfiguration(speed=speed) | ||
|
|
||
| def _search_lowest_eos_feasible_speed( | ||
| self, | ||
| get_outlet_stream: Callable[[float], FluidStream], | ||
| ) -> SpeedConfiguration | Solution[SpeedConfiguration]: | ||
| """Binary-search upward for the lowest speed where the EOS produces a valid outlet. | ||
|
|
||
| Pre-checks that boundary.max is feasible; if not, returns a failed Solution. | ||
| """ | ||
| logger.debug( | ||
| "Outlet fluid not achievable at minimum speed %.1f — searching for lowest feasible speed.", | ||
| self._boundary.min, | ||
| ) | ||
| try: | ||
| get_outlet_stream(self._boundary.max) | ||
| except OutletFluidNotAchievableError as e: | ||
| return self._fluid_not_achievable_solution(e, SpeedConfiguration(speed=self._boundary.max)) | ||
|
|
||
| def feasible(x: float) -> BinarySearchResult: | ||
| try: | ||
| get_outlet_stream(x) | ||
| return ACCEPT_AND_GO_LOWER | ||
| except (OutletFluidNotAchievableError, RateTooHighError, RateTooLowError): | ||
| return REJECT_AND_GO_HIGHER | ||
|
|
||
| speed = self._search_strategy.search(boundary=self._boundary, func=feasible) | ||
| return SpeedConfiguration(speed=speed) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When max speed fails EOS, this block adjusts
max_speed_configurationto the highest EOS-feasible speed and computesmaximum_speed_outlet_streamat that adjusted speed. If the target is still above that adjusted maximum, the failure branch below currently returns the original boundary max, which makes the returned configuration inconsistent withachievable_valueand can cause callers to re-run the EOS-invalid speed. Consider returningmax_speed_configurationfrom that target-not-achievable branch instead.