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

Optimistic Backjumping #183

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ exclude = [
]

[tool.ruff.lint.mccabe]
max-complexity = 12
max-complexity = 15

[tool.mypy]
warn_unused_configs = true
Expand Down
53 changes: 45 additions & 8 deletions src/resolvelib/resolvers/resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ def __init__(
self._r = reporter
self._states: list[State[RT, CT, KT]] = []

# Optimistic backjumping variables
self._optimistic_backjumping = True
self._save_states: list[State[RT, CT, KT]] | None = None

@property
def state(self) -> State[RT, CT, KT]:
try:
Expand Down Expand Up @@ -324,11 +328,24 @@ def _backjump(self, causes: list[RequirementInformation[RT, CT]]) -> bool:
except (IndexError, KeyError):
raise ResolutionImpossible(causes) from None

# Only backjump if the current broken state is
# an incompatible dependency
if name not in incompatible_deps:
# If optimistic backjumping has switched off only backjump
# when the current candidate is in the incompatible dependencies
safe_backjump = name in incompatible_deps
if not self._optimistic_backjumping and not safe_backjump:
break

# On the first time a non-regular backjump is done the state
# is saved so we can restore it later if the resolution fails
if not safe_backjump and self._save_states is None:
self._save_states = [
State(
mapping=s.mapping.copy(),
criteria=s.criteria.copy(),
backtrack_causes=s.backtrack_causes[:],
)
for s in self._states
]

# If the current dependencies and the incompatible dependencies
# are overlapping then we have found a cause of the incompatibility
current_dependencies = {
Expand Down Expand Up @@ -448,12 +465,32 @@ def resolve(self, requirements: Iterable[RT], max_rounds: int) -> State[RT, CT,
# Backjump if pinning fails. The backjump process puts us in
# an unpinned state, so we can work on it in the next round.
self._r.resolving_conflicts(causes=causes)
success = self._backjump(causes)
self.state.backtrack_causes[:] = causes

# Dead ends everywhere. Give up.
if not success:
raise ResolutionImpossible(self.state.backtrack_causes)
# If optimistic backjumping fails restore previous state
try:
success = self._backjump(causes)
except ResolutionImpossible:
if self._optimistic_backjumping and self._save_states:
failed_optimistic_backjumping = True
else:
raise
else:
failed_optimistic_backjumping = bool(
not success
and self._optimistic_backjumping
and self._save_states
)

if failed_optimistic_backjumping and self._save_states:
self._optimistic_backjumping = False
self._states = self._save_states
self._save_states = None
else:
self.state.backtrack_causes[:] = causes

# Dead ends everywhere. Give up.
if not success:
raise ResolutionImpossible(self.state.backtrack_causes)
else:
# discard as information sources any invalidated names
# (unsatisfied names that were previously satisfied)
Expand Down
Loading