Coverage for bim2sim/kernel/decision/decisionhandler.py: 63%
111 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1"""DecisionHandlers prepare decisions to allow easy answering.
3DecisionHandlers are designed to handle DecisionBunch yielding generators.
5Example:
6 >>> def de_gen():
7 ... decision = StringDecision("Whats your name?")
8 ... yield DecisionBunch([decision])
9 ... print(decision.value)
11 >>> handler = DebugDecisionHandler(["R2D2"])
13 >>> # version 1: no further interaction needed
14 >>> handler.handle(de_gen())
15 "R2D2"
17 >>> # version 2: iterate over decisions and answers and apply them on your own
18 >>> for decision, answer in handler.decision_answer_mapping(de_gen()):
19 ... decision.value = answer
20 "R2D2"
22"""
23import logging
24from abc import ABCMeta
25from typing import Iterable, Generator, Any, Dict
27from bim2sim.kernel.decision import BoolDecision, RealDecision, ListDecision, \
28 StringDecision, \
29 GuidDecision, DecisionBunch
32# TODO: contextmanager (shutdown) or how to make sure shutdown is called?
33class DecisionHandler(metaclass=ABCMeta):
34 """Basic DecisionHandler for decision solving"""
36 def __init__(self):
37 self.logger = logging.getLogger(__name__ + '.DecisionHandler')
38 self.return_value = None
40 def handle(self, decision_gen: Generator['DecisionBunch', None, Any],
41 saved_decisions: Dict[str, Dict[str, Any]] = None) -> Any:
42 """Processes decisions by applying saved answers or mapping new ones.
44 This function iterates over a generator of `DecisionBunch` objects,
45 either applying previously saved decisions from previous project runs
46 or allowing the user to map new decisions based on the provided
47 generator. If saved decisions are provided, it tries to find and apply
48 the corresponding answers to the decisions. If a decision cannot be
49 matched to a saved answer, a `ValueError` is raised.
51 Args:
52 decision_gen (Generator[DecisionBunch, None, Any]):
53 A generator that yields `DecisionBunch` objects.
54 saved_decisions (Dict[str, Dict[str, Any]], optional):
55 A dictionary of saved decisions, where the key is a global
56 decision key and the value is a dictionary containing decision
57 details, including the 'value'. Defaults to None.
59 Returns:
60 Any: The return value is typically determined by the subclass
61 implementation or external logic.
63 Raises:
64 ValueError: If saved decisions are provided but a decision in
65 `decision_gen` does not have a corresponding saved answer.
66 """
67 if saved_decisions:
68 for decision_bunch in decision_gen:
69 for decision in decision_bunch:
70 answer = None
71 if decision.representative_global_keys:
72 for global_key in decision.representative_global_keys:
73 answer = saved_decisions.get(global_key)
74 if answer:
75 break
76 else:
77 answer = saved_decisions.get(decision.global_key)
79 if answer:
80 decision.value = answer['value']
81 else:
82 raise ValueError(
83 f"Saved decisions are provided, but no answer is "
84 f"stored for decision with key "
85 f"'{decision.global_key}'. Please restart "
86 f"the process without using saved decisions."
87 )
88 else:
89 for decision, answer in self.decision_answer_mapping(decision_gen):
90 decision.value = answer
92 return self.return_value
94 def get_answers_for_bunch(self, bunch: DecisionBunch) -> list:
95 """Collect and return answers for given decision bunch."""
96 raise NotImplementedError
98 def decision_answer_mapping(
99 self, decision_generator: Generator[DecisionBunch, None, None]):
100 """
101 Generator method yielding tuples of decision and answer.
103 the return value of decision_generator can be
104 obtained from self.return_value
105 """
106 # We preserve the return value of the generator
107 # by using next and StopIteration instead of just iterating
108 try:
109 while True:
110 decision_bunch = next(decision_generator)
111 answer_bunch = self.get_answers_for_bunch(decision_bunch)
112 yield from zip(decision_bunch, answer_bunch)
113 except StopIteration as generator_return:
114 self.return_value = generator_return.value
116 def get_question(self, decision):
117 return decision.get_question()
119 def get_body(self, decision):
120 return decision.get_body()
122 def get_options(self, decision):
123 return decision.get_options()
125 def validate(self, decision, value):
126 return decision.validate(value)
128 def shutdown(self, success):
129 """Shut down handler"""
130 pass
132 def parse(self, decision, raw_answer):
133 if isinstance(decision, BoolDecision):
134 return self.parse_bool_input(raw_answer)
135 elif isinstance(decision, RealDecision):
136 return self.parse_real_input(raw_answer, decision.unit)
137 elif isinstance(decision, ListDecision):
138 return self.parse_list_input(raw_answer, decision.items)
139 elif isinstance(decision, StringDecision):
140 return self.parse_string_input(raw_answer)
141 elif isinstance(decision, GuidDecision):
142 return self.parse_guid_input(raw_answer)
144 @staticmethod
145 def parse_real_input(raw_input, unit=None):
146 """Convert input to float"""
147 if not isinstance(raw_input, float):
148 raise NotImplementedError("Parsing real not implemented.")
149 try:
150 if unit:
151 value = raw_input * unit
152 else:
153 value = raw_input
154 except:
155 raise NotImplementedError("Parsing real not implemented.")
156 return value
158 @staticmethod
159 def parse_bool_input(raw_input):
160 """Convert input to bool"""
161 if not isinstance(raw_input, bool):
162 raise NotImplementedError("Parsing bool not implemented.")
163 return raw_input
165 @staticmethod
166 def parse_list_input(raw_input, items):
167 try:
168 raw_value = items[raw_input]
169 except Exception:
170 raise NotImplementedError("Parsing list index not implemented.")
171 return raw_value
173 @staticmethod
174 def parse_string_input(raw_input):
175 try:
176 raw_value = str(raw_input)
177 except Exception:
178 raise NotImplementedError("Parsing string not implemented.")
179 return raw_value
181 @staticmethod
182 def parse_guid_input(raw_input):
183 try:
184 raw_value = str(raw_input)
185 except Exception:
186 raise NotImplementedError("Parsing guid not implemented.")
187 return raw_value
190class DebugDecisionHandler(DecisionHandler):
191 """Simply use a predefined list of values as answers."""
193 def __init__(self, answers: Iterable):
194 super().__init__()
195 # turn answers into a generator
196 self.answers = (ans for ans in answers)
197 self.unused_answers = tuple()
199 def get_answers_for_bunch(self, bunch: DecisionBunch) -> list:
200 answers = []
201 try:
202 for decision in bunch:
203 answers.append(next(self.answers))
204 except StopIteration:
205 raise AssertionError(f"Not enough answers provided. First decision with no answer: {decision}")
206 return answers
208 def decision_answer_mapping(self, *args, **kwargs):
209 yield from super().decision_answer_mapping(*args, **kwargs)
210 self.unused_answers = tuple(self.answers)
211 if self.unused_answers:
212 self.logger.warning(f"Following answers were not used: "
213 f"{', '.join(map(str, self.unused_answers))}")