-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce first Problems and Solutions
- Loading branch information
Showing
6 changed files
with
352 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,4 @@ | ||
import re | ||
import typing | ||
import enum | ||
import dataclasses | ||
import datetime | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import dataclasses | ||
import typing | ||
|
||
import numpy as np | ||
|
||
from ..entities.card import BaseCard | ||
from ..entities.model import EstiModel | ||
|
||
|
||
@dataclasses.dataclass(init=False) | ||
class Problem: | ||
description: str | ||
affected_cards_names: list[str] | ||
tags: set[str] | ||
|
||
def __init__(self): | ||
self.description = "" | ||
self.affected_cards_names = [] | ||
self.tags = frozenset() | ||
|
||
def add_tag(self, tag): | ||
self.tags = frozenset(self.tags.union([tag])) | ||
|
||
|
||
class ProblemDetector: | ||
POINT_THRESHOLD = 0.4 | ||
|
||
def __init__(self, model: EstiModel, cards: typing.Iterable[BaseCard]): | ||
self.model = model | ||
self.cards = cards | ||
self.problems = [] | ||
|
||
self._get_problems() | ||
|
||
def _get_problems(self): | ||
for card in self.cards: | ||
self._get_card_problem(card) | ||
|
||
def _get_card_problem(self, card): | ||
self._analyze_inconsistent_record(card) | ||
|
||
def _numbers_differ_significantly(self, lhs, rhs): | ||
if np.abs(lhs - rhs) > self.POINT_THRESHOLD: | ||
return True | ||
|
||
def _create_and_add_card_problem(self, card): | ||
problem = Problem() | ||
problem.affected_cards_names.append(card.name) | ||
self.problems.append(problem) | ||
return problem | ||
|
||
def _card_is_not_estimated_but_has_children(self, card): | ||
return card.children and card.point_cost == 0 | ||
|
||
def _treat_inconsistent_estimate(self, card, computed_nominal_cost, recorded_cost, expected_computed_cost): | ||
problem = self._create_and_add_card_problem(card) | ||
problem.add_tag("inconsistent_estimate") | ||
if computed_nominal_cost == 0: | ||
self._inconsistent_card_missing_estimates(problem, card, recorded_cost) | ||
else: | ||
self._inconsistent_card_differing_estimate(problem, card, recorded_cost, expected_computed_cost) | ||
|
||
def _card_has_no_children_with_children(self, card): | ||
for child in card.children: | ||
if child.children: | ||
return False | ||
return True | ||
|
||
def _inconsistent_card_missing_estimates(self, problem, card, recorded_cost): | ||
problem.description = ( | ||
f"'{card.name}' has inconsistent recorded point cost of {recorded_cost:.2g}, " | ||
f"as its children appear to lack estimations." | ||
) | ||
problem.add_tag("missing_estimates") | ||
if self._card_has_no_children_with_children(card): | ||
problem.add_tag("childless_children") | ||
|
||
def _inconsistent_card_differing_estimate(self, problem, card, recorded_cost, expected_computed_cost): | ||
if expected_computed_cost < recorded_cost: | ||
problem.add_tag("sum_of_children_lower") | ||
problem.description = ( | ||
f"'{card.name}' has inconsistent recorded point cost of {recorded_cost:.2g}, " | ||
f"while the deduced cost is {expected_computed_cost:.2g}" | ||
) | ||
|
||
def _analyze_inconsistent_record(self, card): | ||
recorded_cost = card.point_cost | ||
computed_remaining_cost = self.model.remaining_point_estimate_of(card.name).expected | ||
computed_nominal_cost = self.model.nominal_point_estimate_of(card.name).expected | ||
expected_computed_cost = computed_nominal_cost | ||
if card.children: | ||
expected_computed_cost = computed_remaining_cost | ||
|
||
if self._card_is_not_estimated_but_has_children(card): | ||
return | ||
|
||
if not self._numbers_differ_significantly(recorded_cost, expected_computed_cost): | ||
return | ||
|
||
self._treat_inconsistent_estimate(card, computed_nominal_cost, recorded_cost, expected_computed_cost) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
import dataclasses | ||
import typing | ||
|
||
from ..entities.card import BaseCard | ||
from ..problems import Problem | ||
|
||
|
||
@dataclasses.dataclass(init=False) | ||
class Solution: | ||
card_name: str | ||
|
||
def __init__(self): | ||
self.card_name = "" | ||
|
||
def prime(self, cards: typing.Iterable[BaseCard]): | ||
raise NotImplementedError() | ||
|
||
|
||
class SolutionByUpdating(Solution): | ||
end_value: float | ||
|
||
def __init__(self): | ||
super().__init__() | ||
self.end_value = None | ||
|
||
|
||
class SolutionByUpdatingChildren(SolutionByUpdating): | ||
action = "update_children_points" | ||
|
||
def prime(self, cards: typing.Iterable[BaseCard]): | ||
my_card = [c for c in cards if c.name == self.card_name][0] | ||
self.end_value = my_card.point_cost / len(my_card.children) | ||
pass | ||
|
||
|
||
class SolutionByUpdatingSelf(SolutionByUpdating): | ||
action = "update_points" | ||
|
||
def prime(self, cards: typing.Iterable[BaseCard]): | ||
pass | ||
|
||
|
||
class ProblemSolver: | ||
SOLUTIONS = [] | ||
|
||
def get_solutions(self, problems: typing.Iterable[Problem]): | ||
solutions = [] | ||
for problem in problems: | ||
solution = self.get_solution_of(problem) | ||
if solution: | ||
solutions.append(solution) | ||
return solutions | ||
|
||
def get_solution_of(self, problem: Problem): | ||
for solution in self.SOLUTIONS: | ||
ret = solution(problem) | ||
if ret: | ||
return ret | ||
|
||
|
||
def problem_solution(func): | ||
ProblemSolver.SOLUTIONS.append(func) | ||
return func | ||
|
||
|
||
@problem_solution | ||
def get_solution_of_inconsistent_parent(problem: Problem): | ||
if "inconsistent_estimate" not in problem.tags: | ||
return | ||
if "missing_estimates" in problem.tags: | ||
return | ||
ret = SolutionByUpdatingSelf() | ||
ret.card_name = problem.affected_cards_names[0] | ||
return ret | ||
|
||
|
||
@problem_solution | ||
def get_solution_of_inconsistent_children(problem: Problem): | ||
if "inconsistent_estimate" not in problem.tags: | ||
return | ||
if "missing_estimates" not in problem.tags: | ||
return | ||
if "childless_children" not in problem.tags: | ||
return | ||
|
||
ret = SolutionByUpdatingChildren() | ||
ret.card_name = problem.affected_cards_names[0] | ||
return ret |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import pytest | ||
|
||
import estimage.problems as tm | ||
|
||
|
||
@pytest.fixture | ||
def cards_one_two(): | ||
card_two = tm.BaseCard("two") | ||
card_two.status = "todo" | ||
card_two.point_cost = 1 | ||
|
||
card_one = tm.BaseCard("one") | ||
card_one.status = "todo" | ||
card_one.point_cost = 1 | ||
card_one.add_element(card_two) | ||
return [card_one, card_two] | ||
|
||
|
||
def test_model_picks_no_problem(): | ||
card_one = tm.BaseCard("one") | ||
card_one.status = "todo" | ||
card_one.point_cost = 1 | ||
|
||
problems = get_problems_of_cards([card_one]) | ||
assert len(problems) == 0 | ||
|
||
|
||
def get_problems_of_cards(cards): | ||
model = tm.EstiModel() | ||
comp = cards[0].to_tree(cards) | ||
model.use_composition(comp) | ||
problems = tm.ProblemDetector(model, cards) | ||
return problems.problems | ||
|
||
|
||
def get_problem_of_cards(cards): | ||
problems = get_problems_of_cards(cards) | ||
assert len(problems) == 1 | ||
return problems[0] | ||
|
||
|
||
def test_model_finds_no_problem(cards_one_two): | ||
assert len(get_problems_of_cards(cards_one_two)) == 0 | ||
|
||
|
||
def test_model_tolerates_no_estimate_of_parent(cards_one_two): | ||
card_one, card_two = cards_one_two | ||
card_one.point_cost = 0 | ||
assert len(get_problems_of_cards(cards_one_two)) == 0 | ||
|
||
|
||
def test_model_tolerates_small_inconsistency(cards_one_two): | ||
card_one, card_two = cards_one_two | ||
card_two.point_cost = 1.2 | ||
assert len(get_problems_of_cards(cards_one_two)) == 0 | ||
|
||
|
||
def test_model_notices_basic_inconsistency(cards_one_two): | ||
card_one, card_two = cards_one_two | ||
card_two.point_cost = 2 | ||
problem = get_problem_of_cards(cards_one_two) | ||
assert "inconsistent_estimate" in problem.tags | ||
assert "sum_of_children_lower" not in problem.tags | ||
assert "one" in problem.description | ||
assert "is 2" in problem.description | ||
assert "of 1" in problem.description | ||
|
||
|
||
def test_model_notices_inconsistency_maybe_caused_by_progress(cards_one_two): | ||
card_one, card_two = cards_one_two | ||
card_two.point_cost = 0.5 | ||
problem = get_problem_of_cards(cards_one_two) | ||
assert "inconsistent_estimate" in problem.tags | ||
assert "sum_of_children_lower" in problem.tags | ||
|
||
|
||
def test_model_notices_children_not_estimated(cards_one_two): | ||
card_one, card_two = cards_one_two | ||
|
||
card_two.status = "done" | ||
problem = get_problem_of_cards(cards_one_two) | ||
assert "inconsistent_estimate" in problem.tags | ||
assert "missing_estimates" not in problem.tags | ||
assert "one" in problem.description | ||
assert "not estimated" not in problem.description | ||
|
||
card_two.point_cost = 0 | ||
problem = get_problem_of_cards(cards_one_two) | ||
assert "one" in problem.description | ||
assert "inconsistent_estimate" in problem.tags | ||
assert "missing_estimates" in problem.tags | ||
|
||
|
||
def test_model_finds_status_problem(cards_one_two): | ||
card_one, card_two = cards_one_two | ||
card_two.status = "done" | ||
problems = get_problems_of_cards([card_two]) | ||
assert len(problems) == 0 | ||
|
||
card_two.point_cost = card_one.point_cost | ||
problem = get_problem_of_cards(cards_one_two) | ||
assert "one" in problem.affected_cards_names |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import pytest | ||
|
||
import estimage.solutions as tm | ||
import estimage.problems as problems | ||
|
||
import test_problems as ut | ||
from test_problems import cards_one_two | ||
|
||
|
||
@pytest.fixture | ||
def solver(): | ||
return tm.ProblemSolver() | ||
|
||
|
||
def test_no_problem_no_solution(solver): | ||
assert len(solver.get_solutions([])) == 0 | ||
|
||
|
||
def test_basic_inconsistency_solution(solver, cards_one_two): | ||
card_one, card_two = cards_one_two | ||
card_two.point_cost = 2 | ||
|
||
problem = ut.get_problem_of_cards(cards_one_two) | ||
solutions = solver.get_solutions([problem]) | ||
assert len(solutions) == 1 | ||
solution = solutions[0] | ||
assert solution.action == "update_points" | ||
assert solution.card_name == "one" | ||
solution.prime([card_one]) | ||
# assert solution.end_value == card_two.point_cost | ||
|
||
|
||
def test_update_children_inconsistency_solution(solver, cards_one_two): | ||
card_one, card_two = cards_one_two | ||
card_two.point_cost = 0 | ||
card_three = tm.BaseCard("three") | ||
card_one.add_element(card_three) | ||
|
||
problem = ut.get_problem_of_cards(cards_one_two) | ||
solutions = solver.get_solutions([problem]) | ||
assert len(solutions) == 1 | ||
solution = solutions[0] | ||
assert solution.action == "update_children_points" | ||
assert solution.card_name == "one" | ||
solution.prime([card_one]) | ||
assert solution.end_value == card_one.point_cost / 2.0 | ||
|
||
|
||
def test_update_complex_children_no_solution(solver, cards_one_two): | ||
card_one, card_two = cards_one_two | ||
card_three = problems.BaseCard("three") | ||
card_two.add_element(card_three) | ||
|
||
problem = ut.get_problem_of_cards([card_one]) | ||
solutions = solver.get_solutions([problem]) | ||
assert len(solutions) == 0 |