Skip to content

Commit

Permalink
Introduce first Problems and Solutions
Browse files Browse the repository at this point in the history
  • Loading branch information
matejak committed Feb 13, 2024
1 parent c414d67 commit 15ec284
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 5 deletions.
2 changes: 1 addition & 1 deletion estimage/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .entities.task import TaskModel, MemoryTaskModel
from .entities.composition import Composition, MemoryComposition
from .entities.pollster import Pollster
from .entities.model import EstiModel
from .entities.model import EstiModel, ProblemDetector
from .entities.event import Event, EventManager


Expand Down
2 changes: 0 additions & 2 deletions estimage/entities/card.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import re
import typing
import enum
import dataclasses
import datetime

Expand Down
8 changes: 6 additions & 2 deletions estimage/entities/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ def __init__(self):
self.name_composition_map = dict()

def use_composition(self, composition: Composition):
self.main_composition = composition
self.main_composition.name = ""
if composition.name:
self.main_composition = Composition("")
self.main_composition.add_composition(composition)
else:
self.main_composition = composition
self.main_composition.name = ""

self.name_result_map = dict()
self.name_composition_map = dict()
Expand Down
81 changes: 81 additions & 0 deletions estimage/problems/__init__.py
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}"
)
56 changes: 56 additions & 0 deletions estimage/solutions/__init__.py
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
94 changes: 94 additions & 0 deletions tests/test_problems.py
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

42 changes: 42 additions & 0 deletions tests/test_solutions.py
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

0 comments on commit 15ec284

Please sign in to comment.