-
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
279 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,81 @@ | ||
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 _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 | ||
|
||
problem = self._create_and_add_card_problem(card) | ||
problem.add_tag("inconsistent_estimate") | ||
if computed_nominal_cost == 0: | ||
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") | ||
else: | ||
problem.description = ( | ||
f"'{card.name}' has inconsistent recorded point cost of {recorded_cost:.2g}, " | ||
f"while the deduced cost is {expected_computed_cost:.2g}" | ||
) |
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 dataclasses | ||
import typing | ||
|
||
from ..problems import Problem | ||
|
||
|
||
@dataclasses.dataclass(init=False) | ||
class Solution: | ||
action: str | ||
card_name: str | ||
end_value: float | ||
|
||
def __init__(self): | ||
self.action = "" | ||
self.card_name = "one" | ||
self.end_value = None | ||
|
||
|
||
class ProblemSolver: | ||
|
||
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): | ||
ret = self.get_solution_of_inconsistent_children(problem) | ||
if ret: | ||
return ret | ||
ret = self.get_solution_of_inconsistent_parent(problem) | ||
if ret: | ||
return ret | ||
return | ||
|
||
def get_solution_of_inconsistent_parent(self, problem: Problem): | ||
if "inconsistent_estimate" not in problem.tags: | ||
return | ||
if "missing_estimates" in problem.tags: | ||
return | ||
ret = Solution() | ||
ret.action = "update_parent" | ||
ret.end_value = 2 | ||
return ret | ||
|
||
def get_solution_of_inconsistent_children(self, problem: Problem): | ||
if "inconsistent_estimate" not in problem.tags: | ||
return | ||
if "missing_estimates" not in problem.tags: | ||
return | ||
ret = Solution() | ||
ret.action = "update_children" | ||
ret.end_value = 1 | ||
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,94 @@ | ||
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 "one" in problem.description | ||
assert "is 2" in problem.description | ||
assert "of 1" in problem.description | ||
|
||
|
||
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,42 @@ | ||
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_parent" | ||
assert solution.card_name == "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 | ||
|
||
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" | ||
assert solution.card_name == "one" | ||
assert solution.end_value == card_one.point_cost |