From e139e2b3bcf02864ca15933ae12d6fe6524cbdca Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 7 Jul 2021 14:33:20 -0400 Subject: [PATCH 001/181] Initial commit --- macq/extract/arms.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 macq/extract/arms.py diff --git a/macq/extract/arms.py b/macq/extract/arms.py new file mode 100644 index 00000000..1ee350e3 --- /dev/null +++ b/macq/extract/arms.py @@ -0,0 +1,6 @@ +from ..trace import ObservationLists + + +class ARMS: + def __new__(cls, obs_lists: ObservationLists): + pass From e0250076db1820e3b35f3fa9242fc4e6531fc42b Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 7 Jul 2021 15:57:58 -0400 Subject: [PATCH 002/181] Fix typing --- macq/extract/learned_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macq/extract/learned_action.py b/macq/extract/learned_action.py index 167ec556..b7eaa0d6 100644 --- a/macq/extract/learned_action.py +++ b/macq/extract/learned_action.py @@ -27,7 +27,7 @@ def details(self): string = f"{self.name} {' '.join([o for o in self.obj_params])}" return string - def update_precond(self, fluents: Set[Fluent]): + def update_precond(self, fluents: Set[str]): """Adds preconditions to the action. Args: From 7abf2759791d7a262fc876297e07849b9a1805ed Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 7 Jul 2021 15:58:39 -0400 Subject: [PATCH 003/181] Make partial observation method selection use string instead of full function reference --- macq/observation/partial_observation.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/macq/observation/partial_observation.py b/macq/observation/partial_observation.py index 0b68ef27..205e8cf2 100644 --- a/macq/observation/partial_observation.py +++ b/macq/observation/partial_observation.py @@ -23,25 +23,23 @@ class PartialObservation(Observation): class. """ - def __init__( - self, - step: Step, - method: Union[Callable[[int], Step], Callable[[Set[Fluent]], Step]], - **method_kwargs - ): + def __init__(self, step: Step, method: str, **method_kwargs): """ Creates an PartialObservation object, storing the step. Args: step (Step): The step associated with this observation. - method (function reference): - The method to be used to tokenize the step. + method (str): + The method to be used to tokenize the step. "random" or "same". **method_kwargs (keyword arguments): The arguments to be passed to the corresponding method function. """ super().__init__(index=step.index) - self.step = method(self, step, **method_kwargs) + if method == "random": + self.step = self.random_subset(step, **method_kwargs) + elif method == "same": + self.step = self.same_subset(step, **method_kwargs) def __eq__(self, value): return isinstance(value, PartialObservation) and self.step == value.step From 3dcc1762184e4bdc3d9c4c69f315d88f20f61b40 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 7 Jul 2021 15:59:00 -0400 Subject: [PATCH 004/181] Add arms test --- tests/extract/test_arms.py | 15 +++++++++++++++ tests/extract/test_slaf.py | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 tests/extract/test_arms.py diff --git a/tests/extract/test_arms.py b/tests/extract/test_arms.py new file mode 100644 index 00000000..bd2d8817 --- /dev/null +++ b/tests/extract/test_arms.py @@ -0,0 +1,15 @@ +from macq.extract import Extract, modes +from macq.observation import PartialObservation +from tests.utils.generators import generate_blocks_traces + + +if __name__ == "__main__": + traces = generate_blocks_traces(plan_len=10, num_traces=100) # need a goal + observations = traces.tokenize( + PartialObservation, + method="random", + percent_missing=0.10, + ) + model = Extract(observations, modes.ARMS) + print() + print(model.details()) diff --git a/tests/extract/test_slaf.py b/tests/extract/test_slaf.py index d93fe1c2..74f9cca2 100644 --- a/tests/extract/test_slaf.py +++ b/tests/extract/test_slaf.py @@ -7,7 +7,7 @@ traces = generate_blocks_traces(plan_len=2, num_traces=1) observations = traces.tokenize( PartialObservation, - method=PartialObservation.random_subset, + method="random", percent_missing=0.10, ) model = Extract(observations, modes.SLAF) From 8d4ea67bcaeea8abafb1ce7683574a421d8b2635 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 7 Jul 2021 15:59:26 -0400 Subject: [PATCH 005/181] Add ARMS to Extract --- macq/extract/arms.py | 12 +++++++++++- macq/extract/extract.py | 13 +++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 1ee350e3..5dadcbba 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,6 +1,16 @@ +import macq.extract as extract +from ..observation import PartialObservation from ..trace import ObservationLists class ARMS: + """ARMS model extraction method. + + Extracts a Model from state observations using the ARMS technique. Fluents + are retrieved from the initial state. Actions are learned using the + algorithm. + """ + def __new__(cls, obs_lists: ObservationLists): - pass + if obs_lists.type is not PartialObservation: + raise extract.IncompatibleObservationToken(obs_lists.type, ARMS) diff --git a/macq/extract/extract.py b/macq/extract/extract.py index 00581d62..0e5e7b4e 100644 --- a/macq/extract/extract.py +++ b/macq/extract/extract.py @@ -1,10 +1,13 @@ from dataclasses import dataclass -from typing import List from enum import Enum, auto + +from . import Model +from ..trace import ObservationLists, Action, State + +# Techniques from .observer import Observer from .slaf import Slaf -from ..observation import Observation -from ..trace import ObservationLists, Action, State +from .arms import ARMS @dataclass @@ -29,6 +32,7 @@ class modes(Enum): OBSERVER = auto() SLAF = auto() + ARMS = auto() class Extract: @@ -38,7 +42,7 @@ class Extract: from state observations. """ - def __new__(cls, obs_lists: ObservationLists, mode: modes): + def __new__(cls, obs_lists: ObservationLists, mode: modes) -> Model: """Extracts a Model object. Extracts a model from the observations using the specified extraction @@ -57,6 +61,7 @@ def __new__(cls, obs_lists: ObservationLists, mode: modes): techniques = { modes.OBSERVER: Observer, modes.SLAF: Slaf, + modes.ARMS: ARMS, } if mode == modes.SLAF: # only allow one trace From 79fe6d5b52b7b80547c15d6de6c2ac8d9217a4c7 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 8 Jul 2021 11:35:04 -0400 Subject: [PATCH 006/181] Add functions to ARMS --- macq/extract/__init__.py | 3 +-- macq/extract/arms.py | 33 +++++++++++++++++++++++++++++++++ macq/extract/extract.py | 6 ++---- macq/extract/observer.py | 3 +-- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/macq/extract/__init__.py b/macq/extract/__init__.py index 2245aa60..ba3b6594 100644 --- a/macq/extract/__init__.py +++ b/macq/extract/__init__.py @@ -1,5 +1,5 @@ from .model import Model, LearnedAction -from .extract import Extract, modes, IncompatibleObservationToken, Slaf +from .extract import Extract, modes, IncompatibleObservationToken __all__ = [ "Model", @@ -7,5 +7,4 @@ "modes", "IncompatibleObservationToken", "LearnedAction", - "Slaf", ] diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 5dadcbba..9a98def1 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,3 +1,4 @@ +from typing import Set, List import macq.extract as extract from ..observation import PartialObservation from ..trace import ObservationLists @@ -14,3 +15,35 @@ class ARMS: def __new__(cls, obs_lists: ObservationLists): if obs_lists.type is not PartialObservation: raise extract.IncompatibleObservationToken(obs_lists.type, ARMS) + + # assert that there is a goal + ARMS._check_goal(obs_lists) + # get fluents from initial state + fluents = ARMS._get_fluents(obs_lists) + # call algorithm to get actions + actions = ARMS._get_actions(obs_lists) + return extract.Model(fluents, actions) + + @staticmethod + def _check_goal(obs_lists: ObservationLists) -> bool: + """Checks that there is a goal state in the ObservationLists.""" + goal = False + obs_list: List[PartialObservation] + for obs_list in obs_lists: + obs = obs_list[-1] + # if obs.step.state + return goal + + @staticmethod + def _get_fluents(obs_lists: ObservationLists) -> Set[str]: + """Retrieves the set of fluents in the observations.""" + fluents = set() + obs_list: List[PartialObservation] + for obs_list in obs_lists: + pass + + return fluents + + @staticmethod + def _get_actions(obs_lists: ObservationLists) -> Set[str]: + pass diff --git a/macq/extract/extract.py b/macq/extract/extract.py index 0e5e7b4e..b3d84ba3 100644 --- a/macq/extract/extract.py +++ b/macq/extract/extract.py @@ -64,9 +64,7 @@ def __new__(cls, obs_lists: ObservationLists, mode: modes) -> Model: modes.ARMS: ARMS, } if mode == modes.SLAF: - # only allow one trace - assert ( - len(obs_lists) == 1 - ), "The SLAF extraction technique only takes one trace." + if len(obs_lists) != 1: + raise Exception("The SLAF extraction technique only takes one trace.") return techniques[mode](obs_lists) diff --git a/macq/extract/observer.py b/macq/extract/observer.py index 868e60e6..647e3cd2 100644 --- a/macq/extract/observer.py +++ b/macq/extract/observer.py @@ -3,7 +3,6 @@ from attr import dataclass import macq.extract as extract -from .model import Model from ..trace import ObservationLists from ..observation import IdentityObservation @@ -43,7 +42,7 @@ def __new__(cls, obs_lists: ObservationLists): raise extract.IncompatibleObservationToken(obs_lists.type, Observer) fluents = Observer._get_fluents(obs_lists) actions = Observer._get_actions(obs_lists) - return Model(fluents, actions) + return extract.Model(fluents, actions) @staticmethod def _get_fluents(obs_lists: ObservationLists): From 1478be82d4400f089ade2fcbfccc18f3ffc88bb9 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 8 Jul 2021 16:51:37 -0400 Subject: [PATCH 007/181] Simplify extract imports --- macq/extract/__init__.py | 3 ++- macq/extract/arms.py | 9 +++++---- macq/extract/exceptions.py | 5 +++++ macq/extract/extract.py | 7 ------- macq/extract/observer.py | 9 +++++---- 5 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 macq/extract/exceptions.py diff --git a/macq/extract/__init__.py b/macq/extract/__init__.py index ba3b6594..5818eb9d 100644 --- a/macq/extract/__init__.py +++ b/macq/extract/__init__.py @@ -1,5 +1,6 @@ from .model import Model, LearnedAction -from .extract import Extract, modes, IncompatibleObservationToken +from .extract import Extract, modes +from .exceptions import IncompatibleObservationToken __all__ = [ "Model", diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 02ddbc5e..89419a4c 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,5 +1,6 @@ from typing import Set, List -import macq.extract as extract +from . import LearnedAction, Model +from .exceptions import IncompatibleObservationToken from ..observation import PartialObservation from ..trace import ObservationLists, Fluent @@ -14,7 +15,7 @@ class ARMS: def __new__(cls, obs_lists: ObservationLists): if obs_lists.type is not PartialObservation: - raise extract.IncompatibleObservationToken(obs_lists.type, ARMS) + raise IncompatibleObservationToken(obs_lists.type, ARMS) # assert that there is a goal ARMS._check_goal(obs_lists) @@ -22,7 +23,7 @@ def __new__(cls, obs_lists: ObservationLists): fluents = ARMS._get_fluents(obs_lists) # call algorithm to get actions actions = ARMS._get_actions(obs_lists) - return extract.Model(fluents, actions) + return Model(fluents, actions) @staticmethod def _check_goal(obs_lists: ObservationLists) -> bool: @@ -45,5 +46,5 @@ def _get_fluents(obs_lists: ObservationLists) -> Set[Fluent]: return fluents @staticmethod - def _get_actions(obs_lists: ObservationLists) -> Set[str]: + def _get_actions(obs_lists: ObservationLists) -> Set[LearnedAction]: pass diff --git a/macq/extract/exceptions.py b/macq/extract/exceptions.py new file mode 100644 index 00000000..dac5b626 --- /dev/null +++ b/macq/extract/exceptions.py @@ -0,0 +1,5 @@ +class IncompatibleObservationToken(Exception): + def __init__(self, token, technique, message=None): + if message is None: + message = f"Observations of type {token.__name__} are not compatible with the {technique.__name__} extraction technique." + super().__init__(message) diff --git a/macq/extract/extract.py b/macq/extract/extract.py index b3d84ba3..035e949b 100644 --- a/macq/extract/extract.py +++ b/macq/extract/extract.py @@ -17,13 +17,6 @@ class SAS: post_state: State -class IncompatibleObservationToken(Exception): - def __init__(self, token, technique, message=None): - if message is None: - message = f"Observations of type {token.__name__} are not compatible with the {technique.__name__} extraction technique." - super().__init__(message) - - class modes(Enum): """Model extraction techniques. diff --git a/macq/extract/observer.py b/macq/extract/observer.py index 647e3cd2..de27bb69 100644 --- a/macq/extract/observer.py +++ b/macq/extract/observer.py @@ -2,7 +2,8 @@ from collections import defaultdict from attr import dataclass -import macq.extract as extract +from . import LearnedAction, Model +from .exceptions import IncompatibleObservationToken from ..trace import ObservationLists from ..observation import IdentityObservation @@ -39,10 +40,10 @@ def __new__(cls, obs_lists: ObservationLists): Raised if the observations are not identity observation. """ if obs_lists.type is not IdentityObservation: - raise extract.IncompatibleObservationToken(obs_lists.type, Observer) + raise IncompatibleObservationToken(obs_lists.type, Observer) fluents = Observer._get_fluents(obs_lists) actions = Observer._get_actions(obs_lists) - return extract.Model(fluents, actions) + return Model(fluents, actions) @staticmethod def _get_fluents(obs_lists: ObservationLists): @@ -72,7 +73,7 @@ def _get_actions(obs_lists: ObservationLists): action_transitions = obs_lists.get_all_transitions() for action, transitions in action_transitions.items(): # Create a LearnedAction for the current action - model_action = extract.LearnedAction( + model_action = LearnedAction( action.name, action.obj_params, cost=action.cost ) From 006b226c767b5ed83a76fdd8ecbcf115ebacf80b Mon Sep 17 00:00:00 2001 From: ecal Date: Sat, 10 Jul 2021 13:54:43 -0400 Subject: [PATCH 008/181] Use sets for action obj_params --- macq/extract/learned_action.py | 5 ++--- macq/trace/action.py | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/macq/extract/learned_action.py b/macq/extract/learned_action.py index 799e6a41..1009bd9f 100644 --- a/macq/extract/learned_action.py +++ b/macq/extract/learned_action.py @@ -1,10 +1,9 @@ from __future__ import annotations -from typing import Set, List -from ..trace import Fluent +from typing import Set class LearnedAction: - def __init__(self, name: str, obj_params: List, **kwargs): + def __init__(self, name: str, obj_params: Set, **kwargs): self.name = name self.obj_params = obj_params if "cost" in kwargs: diff --git a/macq/trace/action.py b/macq/trace/action.py index 6df95fab..0a2edddf 100644 --- a/macq/trace/action.py +++ b/macq/trace/action.py @@ -1,5 +1,5 @@ -from typing import List, Set -from .fluent import Fluent, PlanningObject +from typing import Set +from .fluent import PlanningObject class Action: @@ -12,8 +12,8 @@ class Action: Attributes: name (str): The name of the action. - obj_params (list): - The list of objects the action acts on. + obj_params (set): + The set of objects the action acts on. cost (int): The cost to perform the action. """ @@ -21,7 +21,7 @@ class Action: def __init__( self, name: str, - obj_params: List[PlanningObject], + obj_params: Set[PlanningObject], cost: int = 0, ): """Initializes an Action with the parameters provided. From b764b4f152a555dab27df6e4c05a628dfd71b043 Mon Sep 17 00:00:00 2001 From: ecal Date: Sat, 10 Jul 2021 13:54:57 -0400 Subject: [PATCH 009/181] Add getters to ObservationLists --- macq/trace/observation_lists.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index f8243250..3e8e27c2 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -22,6 +22,22 @@ def __init__(self, traces: TraceAPI.TraceList, Token: Type[Observation], **kwarg tokens = trace.tokenize(Token, **kwargs) self.append(tokens) + def get_actions(self): + actions = set() + for obs_list in self: + for obs in obs_list: + action = obs.action + if action: + actions.add(action) + return actions + + def get_fluents(self): + fluents = set() + for obs_list in self: + for obs in obs_list: + fluents.update(list(obs.state.keys())) + return fluents + def fetch_observations(self, query: dict): matches: List[Set[Observation]] = list() trace: List[Observation] @@ -48,12 +64,7 @@ def get_transitions(self, action: str): return self.fetch_observation_windows(query, 0, 1) def get_all_transitions(self): - actions = set() - for trace in self: - for obs in trace: - action = obs.action - if action: - actions.add(action) + actions = self.get_actions() try: return { action: self.get_transitions(action.details()) for action in actions From 9ec0ba4b7dfa08a1f9756f0a0ce1c47e648bfbfc Mon Sep 17 00:00:00 2001 From: ecal Date: Sat, 10 Jul 2021 15:39:24 -0400 Subject: [PATCH 010/181] step 1 --- macq/extract/arms.py | 57 +++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 89419a4c..1d4f06eb 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,7 +1,12 @@ -from typing import Set, List +from collections import defaultdict +from dataclasses import dataclass +from typing import Set, List, Dict +from nnf import Var, And, Or +from pysat.examples.rc2 import RC2 +from pysat.formula import WCNF from . import LearnedAction, Model from .exceptions import IncompatibleObservationToken -from ..observation import PartialObservation +from ..observation import PartialObservation as Observation from ..trace import ObservationLists, Fluent @@ -14,7 +19,7 @@ class ARMS: """ def __new__(cls, obs_lists: ObservationLists): - if obs_lists.type is not PartialObservation: + if obs_lists.type is not Observation: raise IncompatibleObservationToken(obs_lists.type, ARMS) # assert that there is a goal @@ -22,29 +27,47 @@ def __new__(cls, obs_lists: ObservationLists): # get fluents from initial state fluents = ARMS._get_fluents(obs_lists) # call algorithm to get actions - actions = ARMS._get_actions(obs_lists) + actions = ARMS._arms(obs_lists, fluents) return Model(fluents, actions) @staticmethod def _check_goal(obs_lists: ObservationLists) -> bool: """Checks that there is a goal state in the ObservationLists.""" - goal = False - obs_list: List[PartialObservation] - for obs_list in obs_lists: - obs = obs_list[-1] - # if obs.step.state - return goal + # TODO Depends on how Rebecca implements goals + return True @staticmethod def _get_fluents(obs_lists: ObservationLists) -> Set[Fluent]: """Retrieves the set of fluents in the observations.""" - fluents = set() - obs_list: List[PartialObservation] - for obs_list in obs_lists: - pass + return obs_lists.get_fluents() - return fluents + @staticmethod + def _arms(obs_lists: ObservationLists, fluents: Set[Fluent]) -> Set[LearnedAction]: + connected_actions = ARMS._step1(obs_lists) # actions = connected_actions.keys() + # constraints = ARMS._step2(obs_lists, connected_actions, fluents) + + return set() # WARNING temp @staticmethod - def _get_actions(obs_lists: ObservationLists) -> Set[LearnedAction]: - pass + def _step1( + obs_lists: ObservationLists, + ) -> Dict[LearnedAction, Dict[LearnedAction, Set]]: + """Substitute instantiated objects in each action instance with the object type.""" + + actions: List[LearnedAction] = [] + for obs_action in obs_lists.get_actions(): + # We don't support objects with multiple types right now, so no + # multiple type clauses need to be generated + types = {obj.obj_type for obj in obs_action.obj_params} + action = LearnedAction(obs_action.name, types) + actions.append(action) + + connected_actions = {} + for i, a1 in enumerate(actions): + connected_actions[a1] = {} + for a2 in actions[i:]: # includes connecting with self + intersection = a1.obj_params.intersection(a2.obj_params) + if intersection: + connected_actions[a1][a2] = intersection + + return connected_actions From 5538cbcc1f6dbb8e5310d60795ccbaeb13e71f8d Mon Sep 17 00:00:00 2001 From: ecal Date: Sat, 10 Jul 2021 15:40:52 -0400 Subject: [PATCH 011/181] Framework of step 2 --- macq/extract/arms.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 1d4f06eb..9edafbff 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -44,7 +44,7 @@ def _get_fluents(obs_lists: ObservationLists) -> Set[Fluent]: @staticmethod def _arms(obs_lists: ObservationLists, fluents: Set[Fluent]) -> Set[LearnedAction]: connected_actions = ARMS._step1(obs_lists) # actions = connected_actions.keys() - # constraints = ARMS._step2(obs_lists, connected_actions, fluents) + constraints = ARMS._step2(obs_lists, connected_actions, fluents) return set() # WARNING temp @@ -71,3 +71,23 @@ def _step1( connected_actions[a1][a2] = intersection return connected_actions + + @staticmethod + def _step2( + obs_lists: ObservationLists, + connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], + fluents: Set[Fluent], + ) -> List: + """Generate action constraints, information constraints, and plan constraints.""" + + @dataclass + class Relation: + name: str + types: set + + relations: Set = set() + + for action, connections in connected_actions.items(): + pass + + return [] # WARNING temp From 3460b210bbc919658902ed7a66f3f6930c94626f Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 12 Jul 2021 10:45:29 -0400 Subject: [PATCH 012/181] Split step 2 into substeps --- macq/extract/arms.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 9edafbff..ed27408b 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -10,6 +10,15 @@ from ..trace import ObservationLists, Fluent +@dataclass +class Relation: + name: str + types: set + + def __hash__(self): + return hash(self.name + " ".join(self.types)) + + class ARMS: """ARMS model extraction method. @@ -57,7 +66,10 @@ def _step1( actions: List[LearnedAction] = [] for obs_action in obs_lists.get_actions(): # We don't support objects with multiple types right now, so no - # multiple type clauses need to be generated + # multiple type clauses need to be generated. + + # Create LearnedActions for each action, replacing instantiated + # objects with the object type. types = {obj.obj_type for obj in obs_action.obj_params} action = LearnedAction(obs_action.name, types) actions.append(action) @@ -80,14 +92,21 @@ def _step2( ) -> List: """Generate action constraints, information constraints, and plan constraints.""" - @dataclass - class Relation: - name: str - types: set + # Convert fluents to relations with instantiated objects replaced by the object type + relations: Set[Relation] = set( + map( + lambda f: Relation(f.name, set([obj.obj_type for obj in f.objects])), + fluents, + ) + ) - relations: Set = set() - - for action, connections in connected_actions.items(): - pass + action_constraints = ARMS._step2A(connected_actions, fluents) return [] # WARNING temp + + @staticmethod + def _step2A( + connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], + relations: Set[Relation], + ): + pass From 69a390c71d4663fe36e282f0ce47bd80079e1c88 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 12 Jul 2021 11:45:02 -0400 Subject: [PATCH 013/181] Add substeps --- macq/extract/arms.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index ed27408b..dd248ca3 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -100,7 +100,9 @@ def _step2( ) ) - action_constraints = ARMS._step2A(connected_actions, fluents) + action_constraints = ARMS._step2A(connected_actions, relations) + info_constraints = ARMS._step2I(obs_lists) + plan_constraints = ARMS._step2P(obs_lists, connected_actions, relations) return [] # WARNING temp @@ -110,3 +112,15 @@ def _step2A( relations: Set[Relation], ): pass + + @staticmethod + def _step2I(obs_lists: ObservationLists): + pass + + @staticmethod + def _step2P( + obs_lists: ObservationLists, + connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], + relations: Set[Relation], + ): + pass From 728daf70a6064d3647cfc364828b9389708ca5cd Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 12 Jul 2021 12:26:11 -0400 Subject: [PATCH 014/181] Step 2A --- macq/extract/arms.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index dd248ca3..431266a4 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -14,6 +14,7 @@ class Relation: name: str types: set + var: str def __hash__(self): return hash(self.name + " ".join(self.types)) @@ -95,7 +96,11 @@ def _step2( # Convert fluents to relations with instantiated objects replaced by the object type relations: Set[Relation] = set( map( - lambda f: Relation(f.name, set([obj.obj_type for obj in f.objects])), + lambda f: Relation( + f.name, + set([obj.obj_type for obj in f.objects]), + f"{f.name} {' '.join([obj.obj_type for obj in f.objects])}", + ), fluents, ) ) @@ -111,7 +116,40 @@ def _step2A( connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], relations: Set[Relation], ): - pass + def implication(a: str, b: str): + return Or([Var(a).negate(), Var(b)]) + + constraints = [] + actions = set(connected_actions.keys()) + for action in actions: + for relation in relations: + # A relation is relevant to an action if they share parameter types + if relation.types.issubset(action.obj_params): + # A1 + # relation in action.add <=> relation not in action.precond + constraints.append( + implication( + f"{relation.var} in add {action.details()}", + f"{relation.var} notin pre {action.details()}", + ) + ) + constraints.append( + implication( + f"{relation.var} in pre {action.details()}", + f"{relation.var} notin add {action.details()}", + ) + ) + + # A2 + # relation in action.del => relation in action.precond + constraints.append( + implication( + f"{relation.var} in del {action.details()}", + f"{relation.var} in pre {action.details()}", + ) + ) + + return constraints @staticmethod def _step2I(obs_lists: ObservationLists): From f2957f8d32cecad6d3ce0195a77701f77913fcc5 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 15 Jul 2021 19:27:00 -0400 Subject: [PATCH 015/181] Step 2I1 and Step 2I2 --- macq/extract/arms.py | 87 +++++++++++++++++++++++++++++-------- macq/trace/partial_state.py | 6 +-- 2 files changed, 71 insertions(+), 22 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 431266a4..1947377e 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -14,10 +14,12 @@ class Relation: name: str types: set - var: str + + def var(self): + return f"{self.name} {' '.join(list(self.types))}" def __hash__(self): - return hash(self.name + " ".join(self.types)) + return hash(self.var()) class ARMS: @@ -93,21 +95,24 @@ def _step2( ) -> List: """Generate action constraints, information constraints, and plan constraints.""" - # Convert fluents to relations with instantiated objects replaced by the object type - relations: Set[Relation] = set( + # Map fluents to relations + # relations are fluents but with instantiated objects replaced by the object type + relations: Dict[Fluent, Relation] = dict( map( - lambda f: Relation( - f.name, - set([obj.obj_type for obj in f.objects]), - f"{f.name} {' '.join([obj.obj_type for obj in f.objects])}", + lambda f: ( + f, + Relation( + f.name, # the fluent name + set([obj.obj_type for obj in f.objects]), # the object types + ), ), fluents, ) ) - action_constraints = ARMS._step2A(connected_actions, relations) - info_constraints = ARMS._step2I(obs_lists) - plan_constraints = ARMS._step2P(obs_lists, connected_actions, relations) + action_constraints = ARMS._step2A(connected_actions, set(relations.values())) + info_constraints = ARMS._step2I(obs_lists, relations) + # plan_constraints = ARMS._step2P(obs_lists, connected_actions, relations) return [] # WARNING temp @@ -127,16 +132,20 @@ def implication(a: str, b: str): if relation.types.issubset(action.obj_params): # A1 # relation in action.add <=> relation not in action.precond + + # _ is used to mark split locations for parsing later. + # Can't use spaces because both relation.var and + # action.details() contain spaces. constraints.append( implication( - f"{relation.var} in add {action.details()}", - f"{relation.var} notin pre {action.details()}", + f"{relation.var()}_in_add_{action.details()}", + f"{relation.var()}_notin_pre_{action.details()}", ) ) constraints.append( implication( - f"{relation.var} in pre {action.details()}", - f"{relation.var} notin add {action.details()}", + f"{relation.var()}_in_pre_{action.details()}", + f"{relation.var()}_notin_add_{action.details()}", ) ) @@ -144,16 +153,56 @@ def implication(a: str, b: str): # relation in action.del => relation in action.precond constraints.append( implication( - f"{relation.var} in del {action.details()}", - f"{relation.var} in pre {action.details()}", + f"{relation.var()}_in_del_{action.details()}", + f"{relation.var()}_in_pre_{action.details()}", ) ) return constraints @staticmethod - def _step2I(obs_lists: ObservationLists): - pass + def _step2I(obs_lists: ObservationLists, relations: dict): + occurences = defaultdict(int) + constraints = [] + for obs_list in obs_lists: + for i, obs in enumerate(obs_list): + if obs.state is not None: + for fluent, val in obs.state.items(): + # Information constraints only apply to true relations + if val: + # I1 + # relation in the add list of an action <= n (i-1) + i1: List[Var] = [] + for obs_i in obs_list[:i]: + a_i = obs_i.action + i1.append( + Var( + f"{relations[fluent].var()}_in_add_{a_i.details()}" + ) + ) + + # I2 + # relation not in del list of action n (i-1) + i2 = Var( + f"{relations[fluent].var()}_notin_del_{obs_list[i-1].action.details()}" + ) + + constraints.append(And([Or(i1), i2])) + + # I3 + # count occurences + occurences[(relations[fluent], obs.action)] += 1 + + # calculate occurence probabilities + occurence_prob = {} + total_pairs = sum(occurences.keys()) + # Could do this in a map function, but more readable this way + for pair, support_count in occurences.items(): + occurence_prob[pair] = support_count / total_pairs + + # weight of (p,a) is the occurence probability + # if probability > theta, p in pre of a, with weight = + # prior probability @staticmethod def _step2P( diff --git a/macq/trace/partial_state.py b/macq/trace/partial_state.py index f4dc18d6..737e8f98 100644 --- a/macq/trace/partial_state.py +++ b/macq/trace/partial_state.py @@ -1,16 +1,16 @@ from . import State from . import Fluent -from typing import Dict +from typing import Dict, Union class PartialState(State): """A Partial State where the value of some fluents are unknown.""" - def __init__(self, fluents: Dict[Fluent, bool] = {}): + def __init__(self, fluents: Dict[Fluent, Union[bool, None]] = {}): """ Args: fluents (dict): Optional; A mapping of `Fluent` objects to their value in this state. Defaults to an empty `dict`. """ - super().__init__(fluents) + self.fluents = fluents From 96252ba41e8690b6f2e49c314cb64308d53de377 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 16 Jul 2021 16:35:56 -0400 Subject: [PATCH 016/181] Step 2I3 --- macq/extract/arms.py | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 1947377e..8c5c00ee 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -111,7 +111,25 @@ def _step2( ) action_constraints = ARMS._step2A(connected_actions, set(relations.values())) - info_constraints = ARMS._step2I(obs_lists, relations) + info_constraints, support_counts = ARMS._step2I(obs_lists, relations) + """ + # calculate support rates + support_rate = {} + + # NOTE: + # In the paper, Z_Σ_P (denominator of the support rate formula) is + # defined as the "total pairs" in the set of plans. However, in the + # examples it appears that they use the max support count as the + # denominator. My best interpretation is then to use the max support + # count as the denominator to calculate the support rate. + z_sigma_p = max(support_counts.values()) + for pair, support_count in support_counts.items(): + support_rate[pair] = support_count / z_sigma_p + + # weight of (p,a) is the occurence probability + # if probability > theta, p in pre of a, with weight = + # prior probability + """ # plan_constraints = ARMS._step2P(obs_lists, connected_actions, relations) return [] # WARNING temp @@ -162,8 +180,8 @@ def implication(a: str, b: str): @staticmethod def _step2I(obs_lists: ObservationLists, relations: dict): - occurences = defaultdict(int) constraints = [] + support_counts = defaultdict(int) for obs_list in obs_lists: for i, obs in enumerate(obs_list): if obs.state is not None: @@ -191,18 +209,15 @@ def _step2I(obs_lists: ObservationLists, relations: dict): # I3 # count occurences - occurences[(relations[fluent], obs.action)] += 1 + if i < len(obs_list)-1: + # corresponding constraint is related to the current action's precondition list + support_counts[(relations[fluent], obs.action, "pre")] += 1 + else: + # corresponding constraint is related to the previous action's add list + support_counts[(relations[fluent], obs_list[i-1].action, "add")] += 1 - # calculate occurence probabilities - occurence_prob = {} - total_pairs = sum(occurences.keys()) - # Could do this in a map function, but more readable this way - for pair, support_count in occurences.items(): - occurence_prob[pair] = support_count / total_pairs + return constraints, support_counts - # weight of (p,a) is the occurence probability - # if probability > theta, p in pre of a, with weight = - # prior probability @staticmethod def _step2P( From 2fcaf5b0d5fe3570333742700fc271e34c57b1b4 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 19 Jul 2021 16:59:15 -0400 Subject: [PATCH 017/181] Implement the Apriori algorithm --- macq/extract/arms.py | 57 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 8c5c00ee..de980a31 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,6 +1,7 @@ -from collections import defaultdict +from collections import defaultdict, Counter +from itertools import combinations from dataclasses import dataclass -from typing import Set, List, Dict +from typing import Set, List, Dict, Tuple from nnf import Var, And, Or from pysat.examples.rc2 import RC2 from pysat.formula import WCNF @@ -209,15 +210,57 @@ def _step2I(obs_lists: ObservationLists, relations: dict): # I3 # count occurences - if i < len(obs_list)-1: + if i < len(obs_list) - 1: # corresponding constraint is related to the current action's precondition list - support_counts[(relations[fluent], obs.action, "pre")] += 1 + support_counts[ + (relations[fluent], obs.action, "pre") + ] += 1 else: # corresponding constraint is related to the previous action's add list - support_counts[(relations[fluent], obs_list[i-1].action, "add")] += 1 + support_counts[ + (relations[fluent], obs_list[i - 1].action, "add") + ] += 1 return constraints, support_counts + @staticmethod + def apriori(action_lists, minsup) -> Set[Tuple[LearnedAction]]: + counts = Counter( + [action for action_list in action_lists for action in action_list] + ) + # L1 = {actions that appear >minsup} + L1 = set( + frozenset(action) + for action in filter(lambda k: counts[k] >= minsup, counts.keys()) + ) # large 1-itemsets + + # Only going up to L2, so no loop or generalized algorithm needed + # apriori-gen step + C2 = set([i.union(j) for i in L1 for j in L1 if len(i.union(j)) == 2]) + # Since L1 contains 1-itemsets where each item is frequent, C2 can + # only contain valid sets and pruning is not required + + # Get all possible ordered action pairs + C2_ordered = set() + for pair in C2: + pair = list(pair) + C2_ordered.add((pair[0], pair[1])) + C2_ordered.add((pair[1], pair[0])) + + # Count pair occurences and generate L2 + L2 = set() + for a1, a2 in C2_ordered: + count = 0 + for action_list in action_lists: + a1_indecies = [i for i, e in enumerate(action_list) if e == a1] + if a1_indecies: + for i in a1_indecies: + if a2 in action_list[i + 1 :]: + count += 1 + if count >= minsup: + L2.add((a1, a2)) + + return L2 @staticmethod def _step2P( @@ -225,4 +268,6 @@ def _step2P( connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], relations: Set[Relation], ): - pass + frequent_pairs = ARMS.apriori( + [[obs.action for obs in obs_list] for obs_list in obs_lists] + ) From 13efa29ec31ea3d162723818d328ceb8490b6382 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 19 Jul 2021 17:27:33 -0400 Subject: [PATCH 018/181] Add comments and pseudocode for step 2P --- macq/extract/arms.py | 65 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index de980a31..7a2d7a28 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -8,7 +8,7 @@ from . import LearnedAction, Model from .exceptions import IncompatibleObservationToken from ..observation import PartialObservation as Observation -from ..trace import ObservationLists, Fluent +from ..trace import ObservationLists, Fluent, Action # Action only used for typing @dataclass @@ -31,7 +31,14 @@ class ARMS: algorithm. """ - def __new__(cls, obs_lists: ObservationLists): + def __new__(cls, obs_lists: ObservationLists, min_support: int = 2): + """ + Arguments: + obs_lists (ObservationLists): + The observations to extract the model from. + min_support (int): + The minimum support count for an action pair to be considered frequent. + """ if obs_lists.type is not Observation: raise IncompatibleObservationToken(obs_lists.type, ARMS) @@ -40,7 +47,7 @@ def __new__(cls, obs_lists: ObservationLists): # get fluents from initial state fluents = ARMS._get_fluents(obs_lists) # call algorithm to get actions - actions = ARMS._arms(obs_lists, fluents) + actions = ARMS._arms(obs_lists, fluents, min_support) return Model(fluents, actions) @staticmethod @@ -55,9 +62,11 @@ def _get_fluents(obs_lists: ObservationLists) -> Set[Fluent]: return obs_lists.get_fluents() @staticmethod - def _arms(obs_lists: ObservationLists, fluents: Set[Fluent]) -> Set[LearnedAction]: + def _arms( + obs_lists: ObservationLists, fluents: Set[Fluent], min_support: int + ) -> Set[LearnedAction]: connected_actions = ARMS._step1(obs_lists) # actions = connected_actions.keys() - constraints = ARMS._step2(obs_lists, connected_actions, fluents) + constraints = ARMS._step2(obs_lists, connected_actions, fluents, min_support) return set() # WARNING temp @@ -93,6 +102,7 @@ def _step2( obs_lists: ObservationLists, connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], fluents: Set[Fluent], + min_support: int, ) -> List: """Generate action constraints, information constraints, and plan constraints.""" @@ -131,7 +141,9 @@ def _step2( # if probability > theta, p in pre of a, with weight = # prior probability """ - # plan_constraints = ARMS._step2P(obs_lists, connected_actions, relations) + plan_constraints = ARMS._step2P( + obs_lists, connected_actions, set(relations.values()), min_support + ) return [] # WARNING temp @@ -224,13 +236,16 @@ def _step2I(obs_lists: ObservationLists, relations: dict): return constraints, support_counts @staticmethod - def apriori(action_lists, minsup) -> Set[Tuple[LearnedAction]]: + def apriori( + action_lists: List[List[Action]], minsup: int + ) -> Dict[Tuple[Action, Action], int]: + """An implementation of the Apriori algorithm to find frequent ordered pairs of actions.""" counts = Counter( [action for action_list in action_lists for action in action_list] ) # L1 = {actions that appear >minsup} L1 = set( - frozenset(action) + frozenset([action]) for action in filter(lambda k: counts[k] >= minsup, counts.keys()) ) # large 1-itemsets @@ -248,26 +263,46 @@ def apriori(action_lists, minsup) -> Set[Tuple[LearnedAction]]: C2_ordered.add((pair[1], pair[0])) # Count pair occurences and generate L2 - L2 = set() - for a1, a2 in C2_ordered: + frequent_pairs = {} + for ai, aj in C2_ordered: count = 0 for action_list in action_lists: - a1_indecies = [i for i, e in enumerate(action_list) if e == a1] + a1_indecies = [i for i, e in enumerate(action_list) if e == ai] if a1_indecies: for i in a1_indecies: - if a2 in action_list[i + 1 :]: + if aj in action_list[i + 1 :]: count += 1 if count >= minsup: - L2.add((a1, a2)) + frequent_pairs[(ai, aj)] = count - return L2 + return frequent_pairs @staticmethod def _step2P( obs_lists: ObservationLists, connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], relations: Set[Relation], + min_support: int, ): frequent_pairs = ARMS.apriori( - [[obs.action for obs in obs_list] for obs_list in obs_lists] + [[obs.action for obs in obs_list] for obs_list in obs_lists], min_support ) + + for ai, aj in frequent_pairs.keys(): + """ + ∃p( + (p∈ (pre_i ∩ pre_j) ∧ p∉ (del_i)) ∨ + (p∈ (add_i ∩ pre_j)) ∨ + (p∈ (del_i ∩ add_j)) + ) + where p is a relevant relation. + + ∃p can be converted to a disjunction of the formula for all p. + Will need to be converted to CNF at some point. + """ + # get list of relevant relations from connected_actions + # for each relation, save constraint + # connect in a big Or - constraint for pair + + pass + # return constraints with pair support counts From c0d4fad44301e234fae7fe5aa7e6fb3803c02353 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 21 Jul 2021 11:45:21 -0400 Subject: [PATCH 019/181] Step 2P --- macq/extract/arms.py | 116 ++++++++++++++++++++++++++++++++----------- 1 file changed, 86 insertions(+), 30 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 7a2d7a28..3e9a3300 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -65,18 +65,26 @@ def _get_fluents(obs_lists: ObservationLists) -> Set[Fluent]: def _arms( obs_lists: ObservationLists, fluents: Set[Fluent], min_support: int ) -> Set[LearnedAction]: - connected_actions = ARMS._step1(obs_lists) # actions = connected_actions.keys() - constraints = ARMS._step2(obs_lists, connected_actions, fluents, min_support) + connected_actions, learned_actions = ARMS._step1( + obs_lists + ) # actions = connected_actions.keys() + constraints = ARMS._step2( + obs_lists, connected_actions, learned_actions, fluents, min_support + ) return set() # WARNING temp @staticmethod def _step1( obs_lists: ObservationLists, - ) -> Dict[LearnedAction, Dict[LearnedAction, Set]]: + ) -> Tuple[ + Dict[LearnedAction, Dict[LearnedAction, Set]], + Dict[Action, LearnedAction], + ]: """Substitute instantiated objects in each action instance with the object type.""" actions: List[LearnedAction] = [] + learned_actions = {} for obs_action in obs_lists.get_actions(): # We don't support objects with multiple types right now, so no # multiple type clauses need to be generated. @@ -86,6 +94,7 @@ def _step1( types = {obj.obj_type for obj in obs_action.obj_params} action = LearnedAction(obs_action.name, types) actions.append(action) + learned_actions[obs_action] = action connected_actions = {} for i, a1 in enumerate(actions): @@ -95,12 +104,13 @@ def _step1( if intersection: connected_actions[a1][a2] = intersection - return connected_actions + return connected_actions, learned_actions @staticmethod def _step2( obs_lists: ObservationLists, connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], + learned_actions: Dict[Action, LearnedAction], fluents: Set[Fluent], min_support: int, ) -> List: @@ -142,7 +152,11 @@ def _step2( # prior probability """ plan_constraints = ARMS._step2P( - obs_lists, connected_actions, set(relations.values()), min_support + obs_lists, + connected_actions, + learned_actions, + set(relations.values()), + min_support, ) return [] # WARNING temp @@ -204,19 +218,19 @@ def _step2I(obs_lists: ObservationLists, relations: dict): # I1 # relation in the add list of an action <= n (i-1) i1: List[Var] = [] - for obs_i in obs_list[:i]: - a_i = obs_i.action + for obs_i in obs_list[: i - 1]: + ai = obs_i.action i1.append( Var( - f"{relations[fluent].var()}_in_add_{a_i.details()}" + f"{relations[fluent].var()}_in_add_{ai.details()}" ) ) # I2 # relation not in del list of action n (i-1) i2 = Var( - f"{relations[fluent].var()}_notin_del_{obs_list[i-1].action.details()}" - ) + f"{relations[fluent].var()}_in_del_{obs_list[i-1].action.details()}" + ).negate() constraints.append(And([Or(i1), i2])) @@ -225,20 +239,24 @@ def _step2I(obs_lists: ObservationLists, relations: dict): if i < len(obs_list) - 1: # corresponding constraint is related to the current action's precondition list support_counts[ - (relations[fluent], obs.action, "pre") + Var( + f"{relations[fluent].var()}_in_pre_{obs.action.details()}" + ) ] += 1 else: # corresponding constraint is related to the previous action's add list support_counts[ - (relations[fluent], obs_list[i - 1].action, "add") + Var( + f"{relations[fluent].var()}_in_add_{obs_list[i-1].action.details()}" + ) ] += 1 return constraints, support_counts @staticmethod def apriori( - action_lists: List[List[Action]], minsup: int - ) -> Dict[Tuple[Action, Action], int]: + action_lists: List[List[LearnedAction]], minsup: int + ) -> Dict[Tuple[LearnedAction, LearnedAction], int]: """An implementation of the Apriori algorithm to find frequent ordered pairs of actions.""" counts = Counter( [action for action_list in action_lists for action in action_list] @@ -281,28 +299,66 @@ def apriori( def _step2P( obs_lists: ObservationLists, connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], + learned_actions: Dict[Action, LearnedAction], relations: Set[Relation], min_support: int, - ): + ) -> Dict[Or, int]: frequent_pairs = ARMS.apriori( - [[obs.action for obs in obs_list] for obs_list in obs_lists], min_support + [ + [learned_actions[obs.action] for obs in obs_list] + for obs_list in obs_lists + ], + min_support, ) + constraints: Dict[Or, int] = {} for ai, aj in frequent_pairs.keys(): - """ - ∃p( - (p∈ (pre_i ∩ pre_j) ∧ p∉ (del_i)) ∨ - (p∈ (add_i ∩ pre_j)) ∨ - (p∈ (del_i ∩ add_j)) - ) - where p is a relevant relation. - - ∃p can be converted to a disjunction of the formula for all p. - Will need to be converted to CNF at some point. - """ # get list of relevant relations from connected_actions + if ai in connected_actions.keys(): + relevant_relations = connected_actions[ai][aj] + else: + relevant_relations = connected_actions[aj][ai] + + # if the actions are not related they are not a valid pair for a plan constraint + if not relevant_relations: + continue + # for each relation, save constraint - # connect in a big Or - constraint for pair + relation_constraints = [] + for relation in relevant_relations: + """ + ∃p( + (p∈ (pre_i ∩ pre_j) ∧ p∉ (del_i)) ∨ + (p∈ (add_i ∩ pre_j)) ∨ + (p∈ (del_i ∩ add_j)) + ) + where p is a relevant relation. + """ + relation_constraints.append( + Or( + [ + And( + [ + Var(f"{relation.var()}_in_pre_{ai.details()}"), + Var(f"{relation.var()}_in_pre_{aj.details()}"), + Var(f"{relation.var()}_notin_del_{ai.details()}"), + ] + ), + And( + [ + Var(f"{relation.var()}_in_add_{ai.details()}"), + Var(f"{relation.var()}_in_pre_{aj.details()}"), + ] + ), + And( + [ + Var(f"{relation.var()}_in_del_{ai.details()}"), + Var(f"{relation.var()}_in_add_{aj.details()}"), + ] + ), + ] + ) + ) + constraints[Or(relation_constraints)] = frequent_pairs[(ai, aj)] - pass - # return constraints with pair support counts + return constraints From 97b59b0ee5c55df8c5526d721f34f0b94d573d08 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 21 Jul 2021 12:03:33 -0400 Subject: [PATCH 020/181] Add more typing, start step 3 --- macq/extract/arms.py | 45 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 3e9a3300..277c3f0a 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -23,6 +23,14 @@ def __hash__(self): return hash(self.var()) +@dataclass +class ARMSConstraints: + action: List[Or] + info: List[And] + info3: Dict[Var, int] + plan: Dict[Or, int] + + class ARMS: """ARMS model extraction method. @@ -71,6 +79,7 @@ def _arms( constraints = ARMS._step2( obs_lists, connected_actions, learned_actions, fluents, min_support ) + max_sat = ARMS._step3(constraints) return set() # WARNING temp @@ -113,7 +122,7 @@ def _step2( learned_actions: Dict[Action, LearnedAction], fluents: Set[Fluent], min_support: int, - ) -> List: + ) -> ARMSConstraints: """Generate action constraints, information constraints, and plan constraints.""" # Map fluents to relations @@ -132,25 +141,7 @@ def _step2( ) action_constraints = ARMS._step2A(connected_actions, set(relations.values())) - info_constraints, support_counts = ARMS._step2I(obs_lists, relations) - """ - # calculate support rates - support_rate = {} - - # NOTE: - # In the paper, Z_Σ_P (denominator of the support rate formula) is - # defined as the "total pairs" in the set of plans. However, in the - # examples it appears that they use the max support count as the - # denominator. My best interpretation is then to use the max support - # count as the denominator to calculate the support rate. - z_sigma_p = max(support_counts.values()) - for pair, support_count in support_counts.items(): - support_rate[pair] = support_count / z_sigma_p - - # weight of (p,a) is the occurence probability - # if probability > theta, p in pre of a, with weight = - # prior probability - """ + info_constraints, info_support_counts = ARMS._step2I(obs_lists, relations) plan_constraints = ARMS._step2P( obs_lists, connected_actions, @@ -159,13 +150,15 @@ def _step2( min_support, ) - return [] # WARNING temp + return ARMSConstraints( + action_constraints, info_constraints, info_support_counts, plan_constraints + ) @staticmethod def _step2A( connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], relations: Set[Relation], - ): + ) -> List[Or]: def implication(a: str, b: str): return Or([Var(a).negate(), Var(b)]) @@ -206,7 +199,9 @@ def implication(a: str, b: str): return constraints @staticmethod - def _step2I(obs_lists: ObservationLists, relations: dict): + def _step2I( + obs_lists: ObservationLists, relations: dict + ) -> Tuple[List[And], Dict[Var, int]]: constraints = [] support_counts = defaultdict(int) for obs_list in obs_lists: @@ -362,3 +357,7 @@ def _step2P( constraints[Or(relation_constraints)] = frequent_pairs[(ai, aj)] return constraints + + @staticmethod + def _step3(constraints: ARMSConstraints): + pass From 9e945f77ae0a5b736a7aed5ac5634b1add190776 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 21 Jul 2021 13:01:31 -0400 Subject: [PATCH 021/181] Use negate instead of parsing negatives --- macq/extract/arms.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 277c3f0a..3b322407 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -159,8 +159,8 @@ def _step2A( connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], relations: Set[Relation], ) -> List[Or]: - def implication(a: str, b: str): - return Or([Var(a).negate(), Var(b)]) + def implication(a: Var, b: Var): + return Or([a.negate(), b]) constraints = [] actions = set(connected_actions.keys()) @@ -176,14 +176,14 @@ def implication(a: str, b: str): # action.details() contain spaces. constraints.append( implication( - f"{relation.var()}_in_add_{action.details()}", - f"{relation.var()}_notin_pre_{action.details()}", + Var(f"{relation.var()}_in_add_{action.details()}"), + Var(f"{relation.var()}_in_pre_{action.details()}").negate(), ) ) constraints.append( implication( - f"{relation.var()}_in_pre_{action.details()}", - f"{relation.var()}_notin_add_{action.details()}", + Var(f"{relation.var()}_in_pre_{action.details()}"), + Var(f"{relation.var()}_in_add_{action.details()}").negate(), ) ) @@ -191,8 +191,8 @@ def implication(a: str, b: str): # relation in action.del => relation in action.precond constraints.append( implication( - f"{relation.var()}_in_del_{action.details()}", - f"{relation.var()}_in_pre_{action.details()}", + Var(f"{relation.var()}_in_del_{action.details()}"), + Var(f"{relation.var()}_in_pre_{action.details()}"), ) ) @@ -249,7 +249,7 @@ def _step2I( return constraints, support_counts @staticmethod - def apriori( + def _apriori( action_lists: List[List[LearnedAction]], minsup: int ) -> Dict[Tuple[LearnedAction, LearnedAction], int]: """An implementation of the Apriori algorithm to find frequent ordered pairs of actions.""" @@ -298,7 +298,7 @@ def _step2P( relations: Set[Relation], min_support: int, ) -> Dict[Or, int]: - frequent_pairs = ARMS.apriori( + frequent_pairs = ARMS._apriori( [ [learned_actions[obs.action] for obs in obs_list] for obs_list in obs_lists @@ -336,7 +336,9 @@ def _step2P( [ Var(f"{relation.var()}_in_pre_{ai.details()}"), Var(f"{relation.var()}_in_pre_{aj.details()}"), - Var(f"{relation.var()}_notin_del_{ai.details()}"), + Var( + f"{relation.var()}_in_del_{ai.details()}" + ).negate(), ] ), And( @@ -359,5 +361,7 @@ def _step2P( return constraints @staticmethod - def _step3(constraints: ARMSConstraints): + def _step3(constraints: ARMSConstraints) -> WCNF: + # translate action constraints to pysat constraints with constant weight + # pass From 4f150c2ec102e23accda2c342436736a92169937 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 21 Jul 2021 13:03:21 -0400 Subject: [PATCH 022/181] Add pysat WCNF encoder based on encoder in python-nnf --- macq/utils/pysat.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 macq/utils/pysat.py diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py new file mode 100644 index 00000000..d9eb9f6c --- /dev/null +++ b/macq/utils/pysat.py @@ -0,0 +1,25 @@ +from typing import List, Tuple, Dict, Hashable +from pysat.formula import WCNF +from nnf import And, Or, Var + + +def _encode(clauses: And[Or[Var]]) -> Tuple[List[List[int]], Dict[int, Hashable]]: + decode = dict(enumerate(clauses.vars(), start=1)) + encode = {v: k for k, v in decode.items()} + + encoded = [ + [encode[var.name] if var.true else -encode[var.name] for var in clause] + for clause in clauses + ] + + return encoded, decode + + +def to_wcnf( + clauses: And[Or[Var]], weights: List[int] +) -> Tuple[WCNF, Dict[int, Hashable]]: + """Converts a python-nnf CNF formula to a pysat WCNF.""" + encoded, decode = _encode(clauses) + wcnf = WCNF() + wcnf.extend(encoded, weights) + return wcnf, decode From cab1b11591e03d91f6d38095952534ae6998f60c Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 21 Jul 2021 17:21:37 -0400 Subject: [PATCH 023/181] Seperate weights from constraints, calculate special weights --- macq/extract/arms.py | 75 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 3b322407..db71392c 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,7 +1,7 @@ from collections import defaultdict, Counter from itertools import combinations from dataclasses import dataclass -from typing import Set, List, Dict, Tuple +from typing import Set, List, Dict, Tuple, Union from nnf import Var, And, Or from pysat.examples.rc2 import RC2 from pysat.formula import WCNF @@ -27,7 +27,7 @@ def __hash__(self): class ARMSConstraints: action: List[Or] info: List[And] - info3: Dict[Var, int] + info3: Dict[Or, int] plan: Dict[Or, int] @@ -201,7 +201,7 @@ def implication(a: Var, b: Var): @staticmethod def _step2I( obs_lists: ObservationLists, relations: dict - ) -> Tuple[List[And], Dict[Var, int]]: + ) -> Tuple[List[And], Dict[Or, int]]: constraints = [] support_counts = defaultdict(int) for obs_list in obs_lists: @@ -234,15 +234,23 @@ def _step2I( if i < len(obs_list) - 1: # corresponding constraint is related to the current action's precondition list support_counts[ - Var( - f"{relations[fluent].var()}_in_pre_{obs.action.details()}" + Or( + [ + Var( + f"{relations[fluent].var()}_in_pre_{obs.action.details()}" + ) + ] ) ] += 1 else: # corresponding constraint is related to the previous action's add list support_counts[ - Var( - f"{relations[fluent].var()}_in_add_{obs_list[i-1].action.details()}" + Or( + [ + Var( + f"{relations[fluent].var()}_in_add_{obs_list[i-1].action.details()}" + ) + ] ) ] += 1 @@ -361,7 +369,52 @@ def _step2P( return constraints @staticmethod - def _step3(constraints: ARMSConstraints) -> WCNF: - # translate action constraints to pysat constraints with constant weight - # - pass + def _step3( + constraints: ARMSConstraints, + action_weight: int, + info_weight: int, + threshold: float, + info3_default: int, + plan_default: int, + ) -> WCNF: + # construct (ordered) problem + # construct ordered weights list + + action_weights = [action_weight] * len(constraints.action) + info_weights = [info_weight] * len(constraints.info) + info3_weights = ARMS._calculate_support_rates( + list(constraints.info3.values()), threshold, info3_default + ) + plan_weights = ARMS._calculate_support_rates( + list(constraints.plan.values()), threshold, plan_default + ) + weights = action_weights + info_weights + info3_weights + plan_weights + + info3_constraints = list(constraints.info3.keys()) + plan_constraints = list(constraints.plan.keys()) + problem = And( + *constraints.action, + *constraints.info, + *info3_constraints, + *plan_constraints, + ) + return # type: ignore + + @staticmethod + def _calculate_support_rates( + support_counts: List[int], threshold: float, default: int + ) -> List[int]: + # NOTE: + # In the paper, Z_Σ_P (denominator of the support rate formula) is + # defined as the "total pairs" in the set of plans. However, in the + # examples it appears that they use the max support count as the + # denominator. My best interpretation is then to use the max support + # count as the denominator to calculate the support rate. + + z_sigma_p = max(support_counts) + + def get_support_rate(count): + probability = count / z_sigma_p + return probability * 100 if probability > threshold else default + + return list(map(get_support_rate, support_counts)) From 6e77f4c1c548034971ba7704718e46f72d162d43 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 21 Jul 2021 18:18:41 -0400 Subject: [PATCH 024/181] Add args, check for valid threshold --- macq/extract/arms.py | 61 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index db71392c..9174a932 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,5 +1,4 @@ from collections import defaultdict, Counter -from itertools import combinations from dataclasses import dataclass from typing import Set, List, Dict, Tuple, Union from nnf import Var, And, Or @@ -39,23 +38,61 @@ class ARMS: algorithm. """ - def __new__(cls, obs_lists: ObservationLists, min_support: int = 2): + class InvalidThreshold(Exception): + def __init__(self, threshold): + super().__init__( + f"Invalid threshold value: {threshold}. Threshold must be a float between 0-1 (inclusive)." + ) + + def __new__( + cls, + obs_lists: ObservationLists, + min_support: int = 2, + action_weight: int = 110, + info_weight: int = 100, + threshold: float = 0.6, + info3_default: int = 30, + plan_default: int = 30, + ): """ Arguments: obs_lists (ObservationLists): The observations to extract the model from. min_support (int): The minimum support count for an action pair to be considered frequent. + action_weight (int): + The constant weight W_A(a) to assign to each action constraint. + info_weight (int): + The constant weight W_I(r) to assign to each information constraint. + threshold (float): + (0-1) The probability threshold θ to determine if an I3/plan constraint + is weighted by its probability or set to a default value. + info3_default (int): + The default weight for I3 constraints with probability below the threshold. + plan_default (int): + The default weight for plan constraints with probability below the threshold. """ if obs_lists.type is not Observation: raise IncompatibleObservationToken(obs_lists.type, ARMS) + if not (threshold >= 0 and threshold <= 1): + raise ARMS.InvalidThreshold(threshold) + # assert that there is a goal ARMS._check_goal(obs_lists) # get fluents from initial state fluents = ARMS._get_fluents(obs_lists) # call algorithm to get actions - actions = ARMS._arms(obs_lists, fluents, min_support) + actions = ARMS._arms( + obs_lists, + fluents, + min_support, + action_weight, + info_weight, + threshold, + info3_default, + plan_default, + ) return Model(fluents, actions) @staticmethod @@ -71,7 +108,14 @@ def _get_fluents(obs_lists: ObservationLists) -> Set[Fluent]: @staticmethod def _arms( - obs_lists: ObservationLists, fluents: Set[Fluent], min_support: int + obs_lists: ObservationLists, + fluents: Set[Fluent], + min_support: int, + action_weight: int, + info_weight: int, + threshold: float, + info3_default: int, + plan_default: int, ) -> Set[LearnedAction]: connected_actions, learned_actions = ARMS._step1( obs_lists @@ -79,7 +123,14 @@ def _arms( constraints = ARMS._step2( obs_lists, connected_actions, learned_actions, fluents, min_support ) - max_sat = ARMS._step3(constraints) + max_sat = ARMS._step3( + constraints, + action_weight, + info_weight, + threshold, + info3_default, + plan_default, + ) return set() # WARNING temp From 45c831be1119001343634da6593fe4996495a52b Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 22 Jul 2021 11:49:53 -0400 Subject: [PATCH 025/181] Step 3 --- macq/extract/arms.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 9174a932..63ac7093 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,6 +1,6 @@ from collections import defaultdict, Counter from dataclasses import dataclass -from typing import Set, List, Dict, Tuple, Union +from typing import Set, List, Dict, Tuple, Union, Hashable from nnf import Var, And, Or from pysat.examples.rc2 import RC2 from pysat.formula import WCNF @@ -8,6 +8,7 @@ from .exceptions import IncompatibleObservationToken from ..observation import PartialObservation as Observation from ..trace import ObservationLists, Fluent, Action # Action only used for typing +from ..utils.pysat import to_wcnf @dataclass @@ -427,9 +428,8 @@ def _step3( threshold: float, info3_default: int, plan_default: int, - ) -> WCNF: - # construct (ordered) problem - # construct ordered weights list + ) -> Tuple[WCNF, Dict[int, Hashable]]: + """Construct the weighted MAX-SAT problem.""" action_weights = [action_weight] * len(constraints.action) info_weights = [info_weight] * len(constraints.info) @@ -449,7 +449,8 @@ def _step3( *info3_constraints, *plan_constraints, ) - return # type: ignore + wcnf, decode = to_wcnf(problem, weights) + return wcnf, decode @staticmethod def _calculate_support_rates( From 3017910fe0ed59399c978907bb94a8f7d01bc546 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 22 Jul 2021 11:56:41 -0400 Subject: [PATCH 026/181] Rename SLAF.py slaf.py --- macq/extract/{SLAF.py => slaf.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename macq/extract/{SLAF.py => slaf.py} (100%) diff --git a/macq/extract/SLAF.py b/macq/extract/slaf.py similarity index 100% rename from macq/extract/SLAF.py rename to macq/extract/slaf.py From 2281d60af53bc2b988675c02c884b4a7d5f25e64 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 22 Jul 2021 14:39:03 -0400 Subject: [PATCH 027/181] Use tarski devel branch --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 90588a66..d7a30417 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ DESCRIPTION = "Action model acquisition from state trace data." DEPENDENCIES = [ - "tarski@git+git://github.com/aig-upf/tarski.git@ffc7e53#egg=tarski[arithmetic]", + "tarski@git+git://github.com/aig-upf/tarski.git@devel#egg=tarski[arithmetic]", "requests", "rich", "python-sat", From 75876190762044bed72beb1797dd25e103ddde53 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 22 Jul 2021 15:02:10 -0400 Subject: [PATCH 028/181] Add comparison function to Trace --- macq/trace/trace.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/macq/trace/trace.py b/macq/trace/trace.py index ddd3d042..104dfe36 100644 --- a/macq/trace/trace.py +++ b/macq/trace/trace.py @@ -53,6 +53,11 @@ def __init__(self, steps: List[Step] = []): self.steps = steps self.__reinit_actions_and_fluents() + def __eq__(self, other): + if not isinstance(other, Trace): + return False + return self.steps == other.steps + def __len__(self): return len(self.steps) From 1da7c485830a3f002da3ad5b26d98d7897840ab4 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 22 Jul 2021 15:11:21 -0400 Subject: [PATCH 029/181] Add test file --- test.py | 174 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 test.py diff --git a/test.py b/test.py new file mode 100644 index 00000000..81bc5853 --- /dev/null +++ b/test.py @@ -0,0 +1,174 @@ +from pathlib import Path +from collections import Counter +from macq.extract import Extract, modes +from macq.observation import IdentityObservation, Observation +from macq.trace import * +from macq import generate, extract +from macq.generate.pddl import VanillaSampling, TraceFromGoal + + +def generate_traces(): + # traces = generate.pddl.VanillaSampling( + # problem_id=2336, plan_len=5, num_traces=3, seed=42 + # ).traces + # traces.generate_more(1) + base = Path(__file__).parent + dom = (base / "tests/pddl_testing_files/blocks_domain.pddl").resolve() + prob = (base / "tests/pddl_testing_files/blocks_problem.pddl").resolve() + traces = VanillaSampling(dom=dom, prob=prob, plan_len=5, num_traces=1).traces # type: ignore + + return traces + + +def extract_model(traces): + observations = traces.tokenize(IdentityObservation) + model = extract.Extract(observations, extract.modes.OBSERVER) + return model + + +def _pysat(): + from pysat.examples.rc2 import RC2 + from pysat.formula import WCNF + from nnf import And, Or, Var + + vars = [Var(f"var{n}") for n in range(1, 4)] + sentence = And([Or([vars[0], vars[1]]), Or([vars[0], vars[2].negate()])]) + + decode = dict(enumerate(sentence.vars(), start=1)) + encode = {v: k for k, v in decode.items()} + clauses = [ + [encode[var.name] if var.true else -encode[var.name] for var in clause] + for clause in sentence + ] + print("decode:", decode) + print("encode:", encode) + print("clauses:", clauses) + + wcnf = WCNF() + wcnf.extend(clauses, weights=[1, 2]) + print("wcnf:", wcnf) + solver = RC2(wcnf) + solver.compute() + print("cost:", solver.cost) + print("model:", solver.model) + print("decoded model:") + for clause in solver.model: # type: ignore + print(" ", decode[abs(clause)], end="") + if clause > 0: + print(" - true") + else: + print(" - false") + print("\nenumerating all models ...") + print("model cost") + for model in solver.enumerate(): + print(model, solver.cost) + + +def apriori(): + minsup = 2 + action_lists = [ + ["a", "b", "c", "d"], + ["a", "c", "b", "c"], + ["b", "c", "a", "d"], + ] + counts = Counter([action for action_list in action_lists for action in action_list]) + # L1 = {actions that appear >minsup} + L1 = set( + frozenset(action) + for action in filter(lambda k: counts[k] >= minsup, counts.keys()) + ) # large 1-itemsets + + # Only going up to L2, so no loop or generalized algorithm needed + # apriori-gen step + C2 = set([i.union(j) for i in L1 for j in L1 if len(i.union(j)) == 2]) + C2_ordered = set() + for pair in C2: + pair = list(pair) + C2_ordered.add((pair[0], pair[1])) + C2_ordered.add((pair[1], pair[0])) + + L2 = set() + for a1, a2 in C2_ordered: + count = 0 + print(f"({a1}, {a2})") + for action_list in action_lists: + print(f"action_list: {action_list}") + a1_indecies = [i for i, e in enumerate(action_list) if e == a1] + print(f"a1_indecies: {a1_indecies}") + if a1_indecies: + for i in a1_indecies: + print("after", i) + if a2 in action_list[i + 1 :]: + print("+1") + count += 1 + if count >= minsup: + print("added") + L2.add((a1, a2)) + print() + print(L2) + # Since L1 contains 1-itemsets where each item is frequent, C2 can + # only contain valid sets and pruning is not required + + +""" +def generate_traces(): + # traces = generate.pddl.VanillaSampling( + # problem_id=2336, plan_len=5, num_traces=3, seed=42 + # ).traces + # traces.generate_more(1) + base = Path(__file__).parent + dom = (base / "tests/pddl_testing_files/blocks_domain.pddl").resolve() + prob = (base / "tests/pddl_testing_files/blocks_problem.pddl").resolve() + traces = VanillaSampling(dom=dom, prob=prob, plan_len=100, num_traces=1).traces # type: ignore + + return traces + + +def extract_model(traces): + observations = traces.tokenize(IdentityObservation) + model = extract.Extract(observations, extract.modes.OBSERVER) + return model +""" + + +def get_fluent(name: str, objs: list[str]): + objects = [PlanningObject(o.split()[0], o.split()[1]) for o in objs] + return Fluent(name, objects) + + +def arms(): + base = Path(__file__).parent + dom = str((base / "tests/pddl_testing_files/blocks_domain.pddl").resolve()) + prob = str((base / "tests/pddl_testing_files/blocks_problem.pddl").resolve()) + + traces = TraceList() + generator = TraceFromGoal(dom=dom, prob=prob) + # for f in generator.trace.fluents: + # print(f) + + generator.change_goal( + { + get_fluent("on", ["object a", "object b"]), + get_fluent("on", ["object b", "object c"]), + } + ) + traces.append(generator.generate_trace()) + generator.change_goal( + { + get_fluent("on", ["object b", "object a"]), + get_fluent("on", ["object c", "object b"]), + } + ) + traces.append(generator.generate_trace()) + traces.print("color") + + +if __name__ == "__main__": + # apriori() + # _pysat() + # traces = generate_traces() + # model = extract_model(traces) + # actions = list(model.actions) + # pre = list(actions[0].precond) + # print(type(pre[0])) + arms() From d9708fcc6390a0565955eff80bc0bce6cf88d9ed Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 22 Jul 2021 15:28:55 -0400 Subject: [PATCH 030/181] Add nnf to dependencies --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d7a30417..d0567b31 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,7 @@ "tarski@git+git://github.com/aig-upf/tarski.git@devel#egg=tarski[arithmetic]", "requests", "rich", + "nnf", "python-sat", "bauhaus", ] From 937a8ad00f7ddc783888413590af794f3a7152d9 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 22 Jul 2021 22:04:44 -0400 Subject: [PATCH 031/181] Add testing --- CONTRIBUTING.md | 2 ++ macq/extract/arms.py | 4 +++- test.py | 13 ++++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eeda14a5..4025d8a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,8 @@ Install macq for development by cloning the repository and running We recommend installing in a virtual environment to avoid package version conflicts. +**`tarski` requires [`clingo`](https://potassco.org/clingo/).** + ### Formatting We use [black](https://black.readthedocs.io/en/stable/) for easy and consistent diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 63ac7093..7a697f66 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -124,7 +124,7 @@ def _arms( constraints = ARMS._step2( obs_lists, connected_actions, learned_actions, fluents, min_support ) - max_sat = ARMS._step3( + max_sat, decode = ARMS._step3( constraints, action_weight, info_weight, @@ -133,6 +133,8 @@ def _arms( plan_default, ) + print(max_sat) + return set() # WARNING temp @staticmethod diff --git a/test.py b/test.py index 81bc5853..7f17269c 100644 --- a/test.py +++ b/test.py @@ -1,10 +1,10 @@ from pathlib import Path from collections import Counter -from macq.extract import Extract, modes -from macq.observation import IdentityObservation, Observation -from macq.trace import * from macq import generate, extract -from macq.generate.pddl import VanillaSampling, TraceFromGoal +from macq.extract import * +from macq.observation import * +from macq.trace import * +from macq.generate.pddl import * def generate_traces(): @@ -160,7 +160,10 @@ def arms(): } ) traces.append(generator.generate_trace()) - traces.print("color") + # traces.print("color") + + observations = traces.tokenize(PartialObservation, percent_missing=0.5) + model = Extract(observations, modes.ARMS) if __name__ == "__main__": From b788885c079dfbb7f48abdf398f9675b41f4fc2c Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 23 Jul 2021 09:34:46 -0400 Subject: [PATCH 032/181] Fix trace bug --- macq/trace/trace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macq/trace/trace.py b/macq/trace/trace.py index 104dfe36..842d2807 100644 --- a/macq/trace/trace.py +++ b/macq/trace/trace.py @@ -42,7 +42,7 @@ class InvalidCostRange(Exception): def __init__(self, message): super().__init__(message) - def __init__(self, steps: List[Step] = []): + def __init__(self, steps: List[Step] = None): """Initializes a Trace with an optional list of steps. Args: @@ -50,7 +50,7 @@ def __init__(self, steps: List[Step] = []): Optional; The list of steps in the trace. Defaults to an empty `list`. """ - self.steps = steps + self.steps = steps if steps is not None else [] self.__reinit_actions_and_fluents() def __eq__(self, other): From 848f62059bcba5e68c7d79d627818a1ab4767684 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 23 Jul 2021 11:38:24 -0400 Subject: [PATCH 033/181] Fix missing actions --- macq/extract/arms.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 7a697f66..361805fc 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -260,7 +260,7 @@ def _step2I( support_counts = defaultdict(int) for obs_list in obs_lists: for i, obs in enumerate(obs_list): - if obs.state is not None: + if obs.state is not None and i > 0: for fluent, val in obs.state.items(): # Information constraints only apply to true relations if val: @@ -362,7 +362,11 @@ def _step2P( ) -> Dict[Or, int]: frequent_pairs = ARMS._apriori( [ - [learned_actions[obs.action] for obs in obs_list] + [ + learned_actions[obs.action] + for obs in obs_list + if obs.action is not None + ] for obs_list in obs_lists ], min_support, From 7c70e122cbc0879bccc1b07a9e7835c108aca976 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 23 Jul 2021 11:39:06 -0400 Subject: [PATCH 034/181] Use connectors to get relevant relations in 2P --- macq/extract/arms.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 361805fc..dba702fe 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -374,17 +374,19 @@ def _step2P( constraints: Dict[Or, int] = {} for ai, aj in frequent_pairs.keys(): + connectors = set() # get list of relevant relations from connected_actions - if ai in connected_actions.keys(): - relevant_relations = connected_actions[ai][aj] - else: - relevant_relations = connected_actions[aj][ai] + if ai in connected_actions and aj in connected_actions[ai]: + connectors.update(connected_actions[ai][aj]) + if aj in connected_actions and ai in connected_actions[aj]: + connectors.update(connected_actions[aj][ai]) # if the actions are not related they are not a valid pair for a plan constraint - if not relevant_relations: + if not connectors: continue # for each relation, save constraint + relevant_relations = {p for p in relations if connectors.issubset(p.types)} relation_constraints = [] for relation in relevant_relations: """ From 3ebf6f592a466813134fc6bb34f234db02bca12a Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 23 Jul 2021 13:27:40 -0400 Subject: [PATCH 035/181] Add more strict typing --- macq/extract/arms.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index dba702fe..81ef6f87 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -25,10 +25,10 @@ def __hash__(self): @dataclass class ARMSConstraints: - action: List[Or] - info: List[And] - info3: Dict[Or, int] - plan: Dict[Or, int] + action: List[Or[Var]] + info: List[And[Union[Or[Var], Var]]] + info3: Dict[Or[Var], int] + plan: Dict[And[Or[Var]], int] class ARMS: @@ -212,11 +212,11 @@ def _step2( def _step2A( connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], relations: Set[Relation], - ) -> List[Or]: + ) -> List[Or[Var]]: def implication(a: Var, b: Var): return Or([a.negate(), b]) - constraints = [] + constraints: List[Or[Var]] = [] actions = set(connected_actions.keys()) for action in actions: for relation in relations: @@ -255,9 +255,9 @@ def implication(a: Var, b: Var): @staticmethod def _step2I( obs_lists: ObservationLists, relations: dict - ) -> Tuple[List[And], Dict[Or, int]]: - constraints = [] - support_counts = defaultdict(int) + ) -> Tuple[List[And[Union[Or[Var], Var]]], Dict[Or[Var], int]]: + constraints: List[And[Union[Or[Var], Var]]] = [] + support_counts: Dict[Or[Var], int] = defaultdict(int) for obs_list in obs_lists: for i, obs in enumerate(obs_list): if obs.state is not None and i > 0: @@ -359,7 +359,7 @@ def _step2P( learned_actions: Dict[Action, LearnedAction], relations: Set[Relation], min_support: int, - ) -> Dict[Or, int]: + ) -> Dict[And[Or[Var]], int]: frequent_pairs = ARMS._apriori( [ [ @@ -372,7 +372,7 @@ def _step2P( min_support, ) - constraints: Dict[Or, int] = {} + constraints: Dict[And[Or[Var]], int] = {} for ai, aj in frequent_pairs.keys(): connectors = set() # get list of relevant relations from connected_actions @@ -387,7 +387,7 @@ def _step2P( # for each relation, save constraint relevant_relations = {p for p in relations if connectors.issubset(p.types)} - relation_constraints = [] + relation_constraints: List[Or[And[Var]]] = [] for relation in relevant_relations: """ ∃p( @@ -452,10 +452,12 @@ def _step3( info3_constraints = list(constraints.info3.keys()) plan_constraints = list(constraints.plan.keys()) problem = And( - *constraints.action, - *constraints.info, - *info3_constraints, - *plan_constraints, + [ + *constraints.action, + *constraints.info, + *info3_constraints, + # *plan_constraints, + ] ) wcnf, decode = to_wcnf(problem, weights) return wcnf, decode From 27ffe0a5e0e69a9679e6ec0d5f8a267a9a666de4 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 23 Jul 2021 13:37:58 -0400 Subject: [PATCH 036/181] Delay conjunction of I1 and I2 constraints --- macq/extract/arms.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 81ef6f87..4cd1b564 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -26,7 +26,7 @@ def __hash__(self): @dataclass class ARMSConstraints: action: List[Or[Var]] - info: List[And[Union[Or[Var], Var]]] + info: List[Or[Var]] info3: Dict[Or[Var], int] plan: Dict[And[Or[Var]], int] @@ -255,8 +255,8 @@ def implication(a: Var, b: Var): @staticmethod def _step2I( obs_lists: ObservationLists, relations: dict - ) -> Tuple[List[And[Union[Or[Var], Var]]], Dict[Or[Var], int]]: - constraints: List[And[Union[Or[Var], Var]]] = [] + ) -> Tuple[List[Or[Var]], Dict[Or[Var], int]]: + constraints: List[Or[Var]] = [] support_counts: Dict[Or[Var], int] = defaultdict(int) for obs_list in obs_lists: for i, obs in enumerate(obs_list): @@ -281,7 +281,8 @@ def _step2I( f"{relations[fluent].var()}_in_del_{obs_list[i-1].action.details()}" ).negate() - constraints.append(And([Or(i1), i2])) + constraints.append(Or(i1)) + constraints.append(Or([i2])) # I3 # count occurences @@ -424,7 +425,8 @@ def _step2P( ] ) ) - constraints[Or(relation_constraints)] = frequent_pairs[(ai, aj)] + # TODO fix + constraints[Or(relation_constraints).to_CNF()] = frequent_pairs[(ai, aj)] return constraints @@ -451,14 +453,15 @@ def _step3( info3_constraints = list(constraints.info3.keys()) plan_constraints = list(constraints.plan.keys()) - problem = And( + problem: And[Or[Var]] = And( [ *constraints.action, *constraints.info, *info3_constraints, - # *plan_constraints, + # *plan_constraints, # TODO fix ] ) + wcnf, decode = to_wcnf(problem, weights) return wcnf, decode From a7e2003dbb25ae71293a8252d98fddf1f4a68d5c Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 23 Jul 2021 14:03:04 -0400 Subject: [PATCH 037/181] Fix pysat translator typing --- macq/utils/pysat.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py index d9eb9f6c..5de892f0 100644 --- a/macq/utils/pysat.py +++ b/macq/utils/pysat.py @@ -1,16 +1,24 @@ -from typing import List, Tuple, Dict, Hashable +from typing import List, Tuple, Dict, Hashable, Union from pysat.formula import WCNF -from nnf import And, Or, Var +from nnf import And, Or, Var, NNF + + +class NotCNF(Exception): + def __init__(self, clauses): + self.clauses = clauses + super().__init__(f"Cannot convert a non CNF formula to WCNF") def _encode(clauses: And[Or[Var]]) -> Tuple[List[List[int]], Dict[int, Hashable]]: decode = dict(enumerate(clauses.vars(), start=1)) encode = {v: k for k, v in decode.items()} - + clauses.simplify() + print(clauses) encoded = [ [encode[var.name] if var.true else -encode[var.name] for var in clause] for clause in clauses ] + print(encoded) return encoded, decode @@ -19,6 +27,8 @@ def to_wcnf( clauses: And[Or[Var]], weights: List[int] ) -> Tuple[WCNF, Dict[int, Hashable]]: """Converts a python-nnf CNF formula to a pysat WCNF.""" + # if not clauses.is_CNF(): + # raise NotCNF(clauses) encoded, decode = _encode(clauses) wcnf = WCNF() wcnf.extend(encoded, weights) From b55d2bcf872a5e5f31e7a8692a43c27a5df5af82 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 23 Jul 2021 16:16:41 -0400 Subject: [PATCH 038/181] Add Step 4 --- macq/extract/arms.py | 75 +++++++++++++++++++++++++------------------- macq/utils/pysat.py | 2 -- test.py | 21 ------------- 3 files changed, 43 insertions(+), 55 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 4cd1b564..6ffc484f 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -28,7 +28,8 @@ class ARMSConstraints: action: List[Or[Var]] info: List[Or[Var]] info3: Dict[Or[Var], int] - plan: Dict[And[Or[Var]], int] + # plan: Dict[And[Or[Var]], int] + plan: Dict[Or[Var], int] class ARMS: @@ -133,7 +134,7 @@ def _arms( plan_default, ) - print(max_sat) + model = ARMS._step4(max_sat) return set() # WARNING temp @@ -360,7 +361,8 @@ def _step2P( learned_actions: Dict[Action, LearnedAction], relations: Set[Relation], min_support: int, - ) -> Dict[And[Or[Var]], int]: + ) -> Dict[Or[Var], int]: + # ) -> Dict[And[Or[Var]], int]: frequent_pairs = ARMS._apriori( [ [ @@ -373,7 +375,8 @@ def _step2P( min_support, ) - constraints: Dict[And[Or[Var]], int] = {} + # constraints: Dict[And[Or[Var]], int] = {} + constraints: Dict[Or[Var], int] = {} for ai, aj in frequent_pairs.keys(): connectors = set() # get list of relevant relations from connected_actions @@ -388,7 +391,8 @@ def _step2P( # for each relation, save constraint relevant_relations = {p for p in relations if connectors.issubset(p.types)} - relation_constraints: List[Or[And[Var]]] = [] + # relation_constraints: List[Or[And[Var]]] = [] + relation_constraints: List[Var] = [] for relation in relevant_relations: """ ∃p( @@ -398,35 +402,34 @@ def _step2P( ) where p is a relevant relation. """ + Phi = Or( + [ + And( + [ + Var(f"{relation.var()}_in_pre_{ai.details()}"), + Var(f"{relation.var()}_in_pre_{aj.details()}"), + Var(f"{relation.var()}_in_del_{ai.details()}").negate(), + ] + ), + And( + [ + Var(f"{relation.var()}_in_add_{ai.details()}"), + Var(f"{relation.var()}_in_pre_{aj.details()}"), + ] + ), + And( + [ + Var(f"{relation.var()}_in_del_{ai.details()}"), + Var(f"{relation.var()}_in_add_{aj.details()}"), + ] + ), + ] + ) relation_constraints.append( - Or( - [ - And( - [ - Var(f"{relation.var()}_in_pre_{ai.details()}"), - Var(f"{relation.var()}_in_pre_{aj.details()}"), - Var( - f"{relation.var()}_in_del_{ai.details()}" - ).negate(), - ] - ), - And( - [ - Var(f"{relation.var()}_in_add_{ai.details()}"), - Var(f"{relation.var()}_in_pre_{aj.details()}"), - ] - ), - And( - [ - Var(f"{relation.var()}_in_del_{ai.details()}"), - Var(f"{relation.var()}_in_add_{aj.details()}"), - ] - ), - ] - ) + Var(f"{relation.var()}_relevant_{ai.details()}_{aj.details()}") ) # TODO fix - constraints[Or(relation_constraints).to_CNF()] = frequent_pairs[(ai, aj)] + constraints[Or(relation_constraints)] = frequent_pairs[(ai, aj)] return constraints @@ -458,11 +461,12 @@ def _step3( *constraints.action, *constraints.info, *info3_constraints, - # *plan_constraints, # TODO fix + *plan_constraints, # TODO fix ] ) wcnf, decode = to_wcnf(problem, weights) + print(len(problem), len(weights)) return wcnf, decode @staticmethod @@ -483,3 +487,10 @@ def get_support_rate(count): return probability * 100 if probability > threshold else default return list(map(get_support_rate, support_counts)) + + @staticmethod + def _step4(max_sat: WCNF): + solver = RC2(max_sat) + solver.compute() + print(solver.model) + return solver.model diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py index 5de892f0..cc36947c 100644 --- a/macq/utils/pysat.py +++ b/macq/utils/pysat.py @@ -13,12 +13,10 @@ def _encode(clauses: And[Or[Var]]) -> Tuple[List[List[int]], Dict[int, Hashable] decode = dict(enumerate(clauses.vars(), start=1)) encode = {v: k for k, v in decode.items()} clauses.simplify() - print(clauses) encoded = [ [encode[var.name] if var.true else -encode[var.name] for var in clause] for clause in clauses ] - print(encoded) return encoded, decode diff --git a/test.py b/test.py index 7f17269c..9c4f08ad 100644 --- a/test.py +++ b/test.py @@ -110,27 +110,6 @@ def apriori(): # only contain valid sets and pruning is not required -""" -def generate_traces(): - # traces = generate.pddl.VanillaSampling( - # problem_id=2336, plan_len=5, num_traces=3, seed=42 - # ).traces - # traces.generate_more(1) - base = Path(__file__).parent - dom = (base / "tests/pddl_testing_files/blocks_domain.pddl").resolve() - prob = (base / "tests/pddl_testing_files/blocks_problem.pddl").resolve() - traces = VanillaSampling(dom=dom, prob=prob, plan_len=100, num_traces=1).traces # type: ignore - - return traces - - -def extract_model(traces): - observations = traces.tokenize(IdentityObservation) - model = extract.Extract(observations, extract.modes.OBSERVER) - return model -""" - - def get_fluent(name: str, objs: list[str]): objects = [PlanningObject(o.split()[0], o.split()[1]) for o in objs] return Fluent(name, objects) From 32469e822c32ff628c798a9ad3a577611e5596cc Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 23 Jul 2021 16:41:05 -0400 Subject: [PATCH 039/181] Remove problem simplification --- macq/extract/arms.py | 8 +------- macq/utils/pysat.py | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 6ffc484f..84254cd7 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -457,16 +457,10 @@ def _step3( info3_constraints = list(constraints.info3.keys()) plan_constraints = list(constraints.plan.keys()) problem: And[Or[Var]] = And( - [ - *constraints.action, - *constraints.info, - *info3_constraints, - *plan_constraints, # TODO fix - ] + constraints.action + constraints.info + info3_constraints + plan_constraints ) wcnf, decode = to_wcnf(problem, weights) - print(len(problem), len(weights)) return wcnf, decode @staticmethod diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py index cc36947c..199d5fce 100644 --- a/macq/utils/pysat.py +++ b/macq/utils/pysat.py @@ -12,7 +12,6 @@ def __init__(self, clauses): def _encode(clauses: And[Or[Var]]) -> Tuple[List[List[int]], Dict[int, Hashable]]: decode = dict(enumerate(clauses.vars(), start=1)) encode = {v: k for k, v in decode.items()} - clauses.simplify() encoded = [ [encode[var.name] if var.true else -encode[var.name] for var in clause] for clause in clauses From 559c1d6b448012ffc524114be6fc1012b9140eff Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 23 Jul 2021 16:41:46 -0400 Subject: [PATCH 040/181] Remove test file --- test.py | 156 -------------------------------------------------------- 1 file changed, 156 deletions(-) delete mode 100644 test.py diff --git a/test.py b/test.py deleted file mode 100644 index 9c4f08ad..00000000 --- a/test.py +++ /dev/null @@ -1,156 +0,0 @@ -from pathlib import Path -from collections import Counter -from macq import generate, extract -from macq.extract import * -from macq.observation import * -from macq.trace import * -from macq.generate.pddl import * - - -def generate_traces(): - # traces = generate.pddl.VanillaSampling( - # problem_id=2336, plan_len=5, num_traces=3, seed=42 - # ).traces - # traces.generate_more(1) - base = Path(__file__).parent - dom = (base / "tests/pddl_testing_files/blocks_domain.pddl").resolve() - prob = (base / "tests/pddl_testing_files/blocks_problem.pddl").resolve() - traces = VanillaSampling(dom=dom, prob=prob, plan_len=5, num_traces=1).traces # type: ignore - - return traces - - -def extract_model(traces): - observations = traces.tokenize(IdentityObservation) - model = extract.Extract(observations, extract.modes.OBSERVER) - return model - - -def _pysat(): - from pysat.examples.rc2 import RC2 - from pysat.formula import WCNF - from nnf import And, Or, Var - - vars = [Var(f"var{n}") for n in range(1, 4)] - sentence = And([Or([vars[0], vars[1]]), Or([vars[0], vars[2].negate()])]) - - decode = dict(enumerate(sentence.vars(), start=1)) - encode = {v: k for k, v in decode.items()} - clauses = [ - [encode[var.name] if var.true else -encode[var.name] for var in clause] - for clause in sentence - ] - print("decode:", decode) - print("encode:", encode) - print("clauses:", clauses) - - wcnf = WCNF() - wcnf.extend(clauses, weights=[1, 2]) - print("wcnf:", wcnf) - solver = RC2(wcnf) - solver.compute() - print("cost:", solver.cost) - print("model:", solver.model) - print("decoded model:") - for clause in solver.model: # type: ignore - print(" ", decode[abs(clause)], end="") - if clause > 0: - print(" - true") - else: - print(" - false") - print("\nenumerating all models ...") - print("model cost") - for model in solver.enumerate(): - print(model, solver.cost) - - -def apriori(): - minsup = 2 - action_lists = [ - ["a", "b", "c", "d"], - ["a", "c", "b", "c"], - ["b", "c", "a", "d"], - ] - counts = Counter([action for action_list in action_lists for action in action_list]) - # L1 = {actions that appear >minsup} - L1 = set( - frozenset(action) - for action in filter(lambda k: counts[k] >= minsup, counts.keys()) - ) # large 1-itemsets - - # Only going up to L2, so no loop or generalized algorithm needed - # apriori-gen step - C2 = set([i.union(j) for i in L1 for j in L1 if len(i.union(j)) == 2]) - C2_ordered = set() - for pair in C2: - pair = list(pair) - C2_ordered.add((pair[0], pair[1])) - C2_ordered.add((pair[1], pair[0])) - - L2 = set() - for a1, a2 in C2_ordered: - count = 0 - print(f"({a1}, {a2})") - for action_list in action_lists: - print(f"action_list: {action_list}") - a1_indecies = [i for i, e in enumerate(action_list) if e == a1] - print(f"a1_indecies: {a1_indecies}") - if a1_indecies: - for i in a1_indecies: - print("after", i) - if a2 in action_list[i + 1 :]: - print("+1") - count += 1 - if count >= minsup: - print("added") - L2.add((a1, a2)) - print() - print(L2) - # Since L1 contains 1-itemsets where each item is frequent, C2 can - # only contain valid sets and pruning is not required - - -def get_fluent(name: str, objs: list[str]): - objects = [PlanningObject(o.split()[0], o.split()[1]) for o in objs] - return Fluent(name, objects) - - -def arms(): - base = Path(__file__).parent - dom = str((base / "tests/pddl_testing_files/blocks_domain.pddl").resolve()) - prob = str((base / "tests/pddl_testing_files/blocks_problem.pddl").resolve()) - - traces = TraceList() - generator = TraceFromGoal(dom=dom, prob=prob) - # for f in generator.trace.fluents: - # print(f) - - generator.change_goal( - { - get_fluent("on", ["object a", "object b"]), - get_fluent("on", ["object b", "object c"]), - } - ) - traces.append(generator.generate_trace()) - generator.change_goal( - { - get_fluent("on", ["object b", "object a"]), - get_fluent("on", ["object c", "object b"]), - } - ) - traces.append(generator.generate_trace()) - # traces.print("color") - - observations = traces.tokenize(PartialObservation, percent_missing=0.5) - model = Extract(observations, modes.ARMS) - - -if __name__ == "__main__": - # apriori() - # _pysat() - # traces = generate_traces() - # model = extract_model(traces) - # actions = list(model.actions) - # pre = list(actions[0].precond) - # print(type(pre[0])) - arms() From 497a457b0d1c55eca1b9301dbd341ea1a3a1ce4d Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 23 Jul 2021 16:42:26 -0400 Subject: [PATCH 041/181] Remove test model --- test_model.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 test_model.json diff --git a/test_model.json b/test_model.json deleted file mode 100644 index 49e9674c..00000000 --- a/test_model.json +++ /dev/null @@ -1 +0,0 @@ -{"fluents": ["(holding object e)", "(on object g object j)", "(on object h object a)", "(on object g object g)", "(on object i object g)", "(on object e object a)", "(on object f object e)", "(holding object f)", "(on object d object j)", "(on object j object e)", "(clear object d)", "(on object f object h)", "(on object c object h)", "(ontable object a)", "(on object d object c)", "(on object i object d)", "(on object h object f)", "(ontable object h)", "(on object e object f)", "(on object j object h)", "(ontable object j)", "(on object g object a)", "(on object c object g)", "(on object b object f)", "(on object g object i)", "(on object c object e)", "(on object a object i)", "(clear object j)", "(clear object b)", "(clear object a)", "(on object h object h)", "(on object h object b)", "(on object b object g)", "(holding object c)", "(holding object b)", "(on object a object h)", "(on object c object d)", "(on object f object c)", "(ontable object c)", "(clear object e)", "(on object j object b)", "(on object h object i)", "(on object c object a)", "(on object b object e)", "(holding object h)", "(clear object g)", "(on object b object a)", "(on object c object b)", "(on object j object c)", "(on object a object g)", "(on object h object g)", "(on object f object a)", "(on object i object f)", "(on object g object c)", "(ontable object d)", "(on object d object f)", "(holding object d)", "(on object g object b)", "(on object b object d)", "(on object d object e)", "(on object e object j)", "(on object e object e)", "(on object d object h)", "(on object e object d)", "(on object i object c)", "(on object e object i)", "(on object h object d)", "(on object h object j)", "(on object c object c)", "(on object g object d)", "(on object b object h)", "(holding object a)", "(on object f object d)", "(on object d object i)", "(on object d object a)", "(ontable object e)", "(on object c object f)", "(holding object j)", "(on object i object i)", "(clear object f)", "(clear object i)", "(on object f object i)", "(ontable object g)", "(on object e object h)", "(on object i object b)", "(on object h object c)", "(on object f object b)", "(on object a object a)", "(on object a object c)", "(on object h object e)", "(on object i object a)", "(holding object i)", "(on object i object j)", "(ontable object b)", "(on object g object h)", "(on object d object b)", "(on object e object c)", "(on object a object b)", "(on object c object j)", "(on object j object f)", "(handempty )", "(on object b object i)", "(on object a object j)", "(on object b object c)", "(ontable object f)", "(on object j object i)", "(on object f object f)", "(on object f object j)", "(on object j object a)", "(on object a object d)", "(on object a object f)", "(on object j object g)", "(on object g object e)", "(on object d object d)", "(on object j object d)", "(on object j object j)", "(on object c object i)", "(on object f object g)", "(on object e object g)", "(on object i object h)", "(on object i object e)", "(on object a object e)", "(on object b object j)", "(on object e object b)", "(clear object c)", "(on object g object f)", "(holding object g)", "(on object b object b)", "(clear object h)", "(ontable object i)", "(on object d object g)"], "actions": [{"name": "put-down", "obj_params": ["object c"], "cost": 0, "precond": ["(on object b object g)", "(ontable object f)", "(holding object c)", "(on object h object a)", "(on object e object j)", "(on object d object i)", "(clear object f)", "(on object a object d)", "(on object j object b)", "(clear object e)", "(ontable object i)", "(on object g object h)"], "add": ["(ontable object c)", "(handempty )", "(clear object c)"], "delete": ["(holding object c)"]}, {"name": "unstack", "obj_params": ["object e", "object c"], "cost": 0, "precond": ["(on object b object g)", "(handempty )", "(ontable object f)", "(on object h object a)", "(on object e object j)", "(on object d object i)", "(clear object f)", "(on object a object d)", "(on object j object b)", "(ontable object i)", "(clear object c)", "(on object c object e)", "(on object g object h)"], "add": ["(clear object e)", "(holding object c)"], "delete": ["(handempty )", "(clear object c)", "(on object c object e)"]}, {"name": "stack", "obj_params": ["object j", "object e"], "cost": 0, "precond": ["(holding object e)", "(on object b object g)", "(ontable object f)", "(on object h object a)", "(clear object j)", "(on object d object i)", "(clear object f)", "(on object a object d)", "(on object j object b)", "(ontable object c)", "(ontable object i)", "(clear object c)", "(on object g object h)"], "add": ["(clear object e)", "(handempty )", "(on object e object j)"], "delete": ["(holding object e)", "(clear object j)"]}, {"name": "unstack", "obj_params": ["object j", "object e"], "cost": 0, "precond": ["(on object b object g)", "(handempty )", "(ontable object f)", "(on object h object a)", "(on object e object j)", "(on object d object i)", "(clear object f)", "(on object a object d)", "(on object j object b)", "(clear object e)", "(ontable object c)", "(ontable object i)", "(clear object c)", "(on object g object h)"], "add": ["(holding object e)", "(clear object j)"], "delete": ["(clear object e)", "(handempty )", "(on object e object j)"]}]} \ No newline at end of file From 74c8e6abe77883a0ca51aa9539ce2bf811dc030f Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 26 Jul 2021 17:26:19 -0400 Subject: [PATCH 042/181] Fix tests --- tests/test_readme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_readme.py b/tests/test_readme.py index 066cd94d..530a529c 100644 --- a/tests/test_readme.py +++ b/tests/test_readme.py @@ -23,7 +23,7 @@ def test_readme(): action1 = traces[0][0].action action1_usage = traces.get_usage(action1) - assert action1_usage == [0.2] * len(traces) + assert action1_usage == [0.2, 0.0, 0.0, 0.0] trace = traces[0] assert len(trace) == 5 From 518a5ec1051678318984c0e2fd27315eb71b4a04 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 26 Jul 2021 17:26:34 -0400 Subject: [PATCH 043/181] Add InconsistentConstraintWeight exception --- macq/extract/exceptions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/macq/extract/exceptions.py b/macq/extract/exceptions.py index dac5b626..47aafe77 100644 --- a/macq/extract/exceptions.py +++ b/macq/extract/exceptions.py @@ -3,3 +3,10 @@ def __init__(self, token, technique, message=None): if message is None: message = f"Observations of type {token.__name__} are not compatible with the {technique.__name__} extraction technique." super().__init__(message) + + +class InconsistentConstraintWeights(Exception): + def __init__(self, constraint, weight1, weight2, message=None): + if message is None: + message = f"Tried to assign the constraint {constraint} conflicting weights ({weight1} and {weight2})" + super().__init__() From 13059e6fe8a3ef41a0d1b776c1b9621910c7c33c Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 26 Jul 2021 17:27:24 -0400 Subject: [PATCH 044/181] Remove duplicate constraints and check for consistent weight assignment --- macq/extract/arms.py | 66 +++++++++++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 84254cd7..f401529c 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -3,9 +3,10 @@ from typing import Set, List, Dict, Tuple, Union, Hashable from nnf import Var, And, Or from pysat.examples.rc2 import RC2 +from pysat.examples.lbx import LBX from pysat.formula import WCNF from . import LearnedAction, Model -from .exceptions import IncompatibleObservationToken +from .exceptions import IncompatibleObservationToken, InconsistentConstraintWeights from ..observation import PartialObservation as Observation from ..trace import ObservationLists, Fluent, Action # Action only used for typing from ..utils.pysat import to_wcnf @@ -402,29 +403,29 @@ def _step2P( ) where p is a relevant relation. """ - Phi = Or( - [ - And( - [ - Var(f"{relation.var()}_in_pre_{ai.details()}"), - Var(f"{relation.var()}_in_pre_{aj.details()}"), - Var(f"{relation.var()}_in_del_{ai.details()}").negate(), - ] - ), - And( - [ - Var(f"{relation.var()}_in_add_{ai.details()}"), - Var(f"{relation.var()}_in_pre_{aj.details()}"), - ] - ), - And( - [ - Var(f"{relation.var()}_in_del_{ai.details()}"), - Var(f"{relation.var()}_in_add_{aj.details()}"), - ] - ), - ] - ) + # Phi = Or( + # [ + # And( + # [ + # Var(f"{relation.var()}_in_pre_{ai.details()}"), + # Var(f"{relation.var()}_in_pre_{aj.details()}"), + # Var(f"{relation.var()}_in_del_{ai.details()}").negate(), + # ] + # ), + # And( + # [ + # Var(f"{relation.var()}_in_add_{ai.details()}"), + # Var(f"{relation.var()}_in_pre_{aj.details()}"), + # ] + # ), + # And( + # [ + # Var(f"{relation.var()}_in_del_{ai.details()}"), + # Var(f"{relation.var()}_in_add_{aj.details()}"), + # ] + # ), + # ] + # ) relation_constraints.append( Var(f"{relation.var()}_relevant_{ai.details()}_{aj.details()}") ) @@ -452,14 +453,27 @@ def _step3( plan_weights = ARMS._calculate_support_rates( list(constraints.plan.values()), threshold, plan_default ) - weights = action_weights + info_weights + info3_weights + plan_weights + all_weights = action_weights + info_weights + info3_weights + plan_weights info3_constraints = list(constraints.info3.keys()) plan_constraints = list(constraints.plan.keys()) - problem: And[Or[Var]] = And( + all_constraints = ( constraints.action + constraints.info + info3_constraints + plan_constraints ) + constraints_w_weights = {} + for constraint, weight in zip(all_constraints, all_weights): + if constraint not in constraints_w_weights: + constraints_w_weights[constraint] = weight + elif weight != constraints_w_weights[constraint]: + raise InconsistentConstraintWeights( + constraint, weight, constraints_w_weights[constraint] + ) + + # dict maintains order, so this should match up properly + problem: And[Or[Var]] = And(list(constraints_w_weights.keys())) + weights = list(constraints_w_weights.keys()) + wcnf, decode = to_wcnf(problem, weights) return wcnf, decode From 93a4df1b222950ebdbe6f440fb3fea874f3ebad3 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 26 Jul 2021 17:34:05 -0400 Subject: [PATCH 045/181] Fix incorrect weight list --- macq/extract/arms.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index f401529c..d32b914e 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -429,7 +429,6 @@ def _step2P( relation_constraints.append( Var(f"{relation.var()}_relevant_{ai.details()}_{aj.details()}") ) - # TODO fix constraints[Or(relation_constraints)] = frequent_pairs[(ai, aj)] return constraints @@ -470,9 +469,8 @@ def _step3( constraint, weight, constraints_w_weights[constraint] ) - # dict maintains order, so this should match up properly problem: And[Or[Var]] = And(list(constraints_w_weights.keys())) - weights = list(constraints_w_weights.keys()) + weights = list(constraints_w_weights.values()) wcnf, decode = to_wcnf(problem, weights) return wcnf, decode From 2143068ab2d69006cdb0767816325a6918b3fbdb Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 26 Jul 2021 17:35:21 -0400 Subject: [PATCH 046/181] Simplify test --- tests/test_readme.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_readme.py b/tests/test_readme.py index 530a529c..0a731b12 100644 --- a/tests/test_readme.py +++ b/tests/test_readme.py @@ -22,8 +22,7 @@ def test_readme(): assert len(traces) == 4 action1 = traces[0][0].action - action1_usage = traces.get_usage(action1) - assert action1_usage == [0.2, 0.0, 0.0, 0.0] + assert traces.get_usage(action1) trace = traces[0] assert len(trace) == 5 From 9ccae5554fada77b2b43e6826aef3c9029554a6b Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 26 Jul 2021 17:48:51 -0400 Subject: [PATCH 047/181] Filter nnf.false from constraints --- macq/extract/arms.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index d32b914e..9ab2a0ff 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,7 +1,7 @@ from collections import defaultdict, Counter from dataclasses import dataclass from typing import Set, List, Dict, Tuple, Union, Hashable -from nnf import Var, And, Or +from nnf import Var, And, Or, false as nnffalse from pysat.examples.rc2 import RC2 from pysat.examples.lbx import LBX from pysat.formula import WCNF @@ -460,8 +460,10 @@ def _step3( constraints.action + constraints.info + info3_constraints + plan_constraints ) - constraints_w_weights = {} + constraints_w_weights: Dict[Or[Var], int] = {} for constraint, weight in zip(all_constraints, all_weights): + if constraint == nnffalse: + continue if constraint not in constraints_w_weights: constraints_w_weights[constraint] = weight elif weight != constraints_w_weights[constraint]: From 5d17d30331f4c5ba0152d235b347e5a90b3586ee Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 26 Jul 2021 18:14:28 -0400 Subject: [PATCH 048/181] Add InvalidMaxSATModel exception --- macq/extract/exceptions.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/macq/extract/exceptions.py b/macq/extract/exceptions.py index 47aafe77..40ea1f7d 100644 --- a/macq/extract/exceptions.py +++ b/macq/extract/exceptions.py @@ -10,3 +10,10 @@ def __init__(self, constraint, weight1, weight2, message=None): if message is None: message = f"Tried to assign the constraint {constraint} conflicting weights ({weight1} and {weight2})" super().__init__() + + +class InvalidMaxSATModel(Exception): + def __init__(self, model, message=None): + if message is None: + message = f"The MAX-SAT solver generated an invalid model. Model should be a list of integers. model = {model}" + super().__init__(message) From 5c684312bbff4cd82e5e0dd3396be4a43fce3108 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 26 Jul 2021 18:15:21 -0400 Subject: [PATCH 049/181] Check for a valid model, strict typing on the model --- macq/extract/arms.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 9ab2a0ff..1b7a09b1 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,12 +1,15 @@ from collections import defaultdict, Counter from dataclasses import dataclass -from typing import Set, List, Dict, Tuple, Union, Hashable +from typing import Set, List, Dict, Tuple, Hashable from nnf import Var, And, Or, false as nnffalse from pysat.examples.rc2 import RC2 -from pysat.examples.lbx import LBX from pysat.formula import WCNF from . import LearnedAction, Model -from .exceptions import IncompatibleObservationToken, InconsistentConstraintWeights +from .exceptions import ( + IncompatibleObservationToken, + InconsistentConstraintWeights, + InvalidMaxSATModel, +) from ..observation import PartialObservation as Observation from ..trace import ObservationLists, Fluent, Action # Action only used for typing from ..utils.pysat import to_wcnf @@ -101,7 +104,7 @@ def __new__( @staticmethod def _check_goal(obs_lists: ObservationLists) -> bool: """Checks that there is a goal state in the ObservationLists.""" - # TODO Depends on how Rebecca implements goals + # TODO return True @staticmethod @@ -136,6 +139,15 @@ def _arms( ) model = ARMS._step4(max_sat) + print(type(model)) + print(model) + + for clause in model: + print(" ", decode[abs(clause)], end="") + if clause > 0: + print(" - true") + else: + print(" - false") return set() # WARNING temp @@ -497,8 +509,10 @@ def get_support_rate(count): return list(map(get_support_rate, support_counts)) @staticmethod - def _step4(max_sat: WCNF): + def _step4(max_sat: WCNF) -> List[int]: solver = RC2(max_sat) solver.compute() - print(solver.model) - return solver.model + model = solver.model + if not isinstance(model, list): + raise InvalidMaxSATModel(model) + return model From 1435261cd476a6549c8e5c1315a2765c936ff46b Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 27 Jul 2021 11:05:29 -0400 Subject: [PATCH 050/181] Remove goal check (goal isn't saved) --- macq/extract/arms.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 1b7a09b1..bb88c53d 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -84,8 +84,6 @@ def __new__( if not (threshold >= 0 and threshold <= 1): raise ARMS.InvalidThreshold(threshold) - # assert that there is a goal - ARMS._check_goal(obs_lists) # get fluents from initial state fluents = ARMS._get_fluents(obs_lists) # call algorithm to get actions @@ -101,12 +99,6 @@ def __new__( ) return Model(fluents, actions) - @staticmethod - def _check_goal(obs_lists: ObservationLists) -> bool: - """Checks that there is a goal state in the ObservationLists.""" - # TODO - return True - @staticmethod def _get_fluents(obs_lists: ObservationLists) -> Set[Fluent]: """Retrieves the set of fluents in the observations.""" @@ -139,7 +131,6 @@ def _arms( ) model = ARMS._step4(max_sat) - print(type(model)) print(model) for clause in model: From cc35ef87e4a116889b808d92ee01862dad8c5367 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 27 Jul 2021 13:06:01 -0400 Subject: [PATCH 051/181] Cleanup pysat typing --- macq/utils/pysat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py index 199d5fce..ee554b48 100644 --- a/macq/utils/pysat.py +++ b/macq/utils/pysat.py @@ -1,6 +1,6 @@ -from typing import List, Tuple, Dict, Hashable, Union +from typing import List, Tuple, Dict, Hashable from pysat.formula import WCNF -from nnf import And, Or, Var, NNF +from nnf import And, Or, Var class NotCNF(Exception): From 9c023145e780ecc3213d2ab78aa1608553e000cf Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 27 Jul 2021 13:07:17 -0400 Subject: [PATCH 052/181] Decode in Step 4 and add Step 5 --- macq/extract/arms.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index bb88c53d..9ae3bc30 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,7 +1,7 @@ from collections import defaultdict, Counter from dataclasses import dataclass from typing import Set, List, Dict, Tuple, Hashable -from nnf import Var, And, Or, false as nnffalse +from nnf import NNF, Var, And, Or, false as nnffalse from pysat.examples.rc2 import RC2 from pysat.formula import WCNF from . import LearnedAction, Model @@ -115,12 +115,14 @@ def _arms( info3_default: int, plan_default: int, ) -> Set[LearnedAction]: - connected_actions, learned_actions = ARMS._step1( - obs_lists - ) # actions = connected_actions.keys() + """The main driver for the ARMS algorithm.""" + + connected_actions, learned_actions = ARMS._step1(obs_lists) + constraints = ARMS._step2( obs_lists, connected_actions, learned_actions, fluents, min_support ) + max_sat, decode = ARMS._step3( constraints, action_weight, @@ -130,15 +132,9 @@ def _arms( plan_default, ) - model = ARMS._step4(max_sat) - print(model) + model = ARMS._step4(max_sat, decode) - for clause in model: - print(" ", decode[abs(clause)], end="") - if clause > 0: - print(" - true") - else: - print(" - false") + action_models = ARMS._step5(model, list(learned_actions.values())) return set() # WARNING temp @@ -500,10 +496,20 @@ def get_support_rate(count): return list(map(get_support_rate, support_counts)) @staticmethod - def _step4(max_sat: WCNF) -> List[int]: + def _step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: solver = RC2(max_sat) solver.compute() - model = solver.model - if not isinstance(model, list): - raise InvalidMaxSATModel(model) + encoded_model = solver.model + if not isinstance(encoded_model, list): + raise InvalidMaxSATModel(encoded_model) + + # decode the model (back to nnf vars) + model: Dict[Hashable, bool] = { + decode[abs(clause)]: clause > 0 for clause in encoded_model + } + return model + + @staticmethod + def _step5(model: Dict[Hashable, bool], actions: List[LearnedAction]): + print(actions[0].details()) From f355338dd8a22a5bb986d5625ed75d0262ef7328 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 27 Jul 2021 13:08:48 -0400 Subject: [PATCH 053/181] Split constraints into its parts --- macq/extract/arms.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 9ae3bc30..61c72b3a 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -512,4 +512,16 @@ def _step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: @staticmethod def _step5(model: Dict[Hashable, bool], actions: List[LearnedAction]): + action_map = {a.details(): a for a in actions} print(actions[0].details()) + for constraint, val in model.items(): + constraint = str(constraint).split("_") + print(constraint) + relation = constraint[0] + ctype = constraint[1] # constraint type + if ctype == "in": + alist = constraint[2] + action = constraint[3] + else: + a1 = constraint[2] + a2 = constraint[3] From 13db5f931588589ab7deadccd64c7c62595f581e Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 27 Jul 2021 18:50:34 -0400 Subject: [PATCH 054/181] Add ConstraintContradiction exception --- macq/extract/exceptions.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/macq/extract/exceptions.py b/macq/extract/exceptions.py index 40ea1f7d..ac51fa01 100644 --- a/macq/extract/exceptions.py +++ b/macq/extract/exceptions.py @@ -8,12 +8,19 @@ def __init__(self, token, technique, message=None): class InconsistentConstraintWeights(Exception): def __init__(self, constraint, weight1, weight2, message=None): if message is None: - message = f"Tried to assign the constraint {constraint} conflicting weights ({weight1} and {weight2})" + message = f"Tried to assign the constraint {constraint} conflicting weights ({weight1} and {weight2})." super().__init__() class InvalidMaxSATModel(Exception): def __init__(self, model, message=None): if message is None: - message = f"The MAX-SAT solver generated an invalid model. Model should be a list of integers. model = {model}" + message = f"The MAX-SAT solver generated an invalid model. Model should be a list of integers. model = {model}." + super().__init__(message) + + +class ConstraintContradiction(Exception): + def __init__(self, fluent, effect, action, message=None): + if message is None: + message = f"Action model has contradictory constraints for {fluent.details()}'s presence in the {effect} list of {action.details()}." super().__init__(message) From ca45acdb9843887416a9f50052b4db209fd564e3 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 27 Jul 2021 18:51:19 -0400 Subject: [PATCH 055/181] Step 5: decode MAX-SAT solution into learned action models --- macq/extract/arms.py | 91 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 71 insertions(+), 20 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 61c72b3a..ebe73278 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -6,6 +6,7 @@ from pysat.formula import WCNF from . import LearnedAction, Model from .exceptions import ( + ConstraintContradiction, IncompatibleObservationToken, InconsistentConstraintWeights, InvalidMaxSATModel, @@ -18,7 +19,7 @@ @dataclass class Relation: name: str - types: set + types: list def var(self): return f"{self.name} {' '.join(list(self.types))}" @@ -119,7 +120,7 @@ def _arms( connected_actions, learned_actions = ARMS._step1(obs_lists) - constraints = ARMS._step2( + constraints, relations = ARMS._step2( obs_lists, connected_actions, learned_actions, fluents, min_support ) @@ -134,8 +135,9 @@ def _arms( model = ARMS._step4(max_sat, decode) - action_models = ARMS._step5(model, list(learned_actions.values())) - + action_models = ARMS._step5( + model, list(learned_actions.values()), list(relations.values()) + ) return set() # WARNING temp @staticmethod @@ -177,7 +179,7 @@ def _step2( learned_actions: Dict[Action, LearnedAction], fluents: Set[Fluent], min_support: int, - ) -> ARMSConstraints: + ) -> Tuple[ARMSConstraints, Dict[Fluent, Relation]]: """Generate action constraints, information constraints, and plan constraints.""" # Map fluents to relations @@ -188,7 +190,7 @@ def _step2( f, Relation( f.name, # the fluent name - set([obj.obj_type for obj in f.objects]), # the object types + [obj.obj_type for obj in f.objects], # the object types ), ), fluents, @@ -196,7 +198,10 @@ def _step2( ) action_constraints = ARMS._step2A(connected_actions, set(relations.values())) - info_constraints, info_support_counts = ARMS._step2I(obs_lists, relations) + info_constraints, info_support_counts = ARMS._step2I( + obs_lists, relations, learned_actions + ) + plan_constraints = ARMS._step2P( obs_lists, connected_actions, @@ -205,8 +210,14 @@ def _step2( min_support, ) - return ARMSConstraints( - action_constraints, info_constraints, info_support_counts, plan_constraints + return ( + ARMSConstraints( + action_constraints, + info_constraints, + info_support_counts, + plan_constraints, + ), + relations, ) @staticmethod @@ -222,7 +233,7 @@ def implication(a: Var, b: Var): for action in actions: for relation in relations: # A relation is relevant to an action if they share parameter types - if relation.types.issubset(action.obj_params): + if set(relation.types).issubset(action.obj_params): # A1 # relation in action.add <=> relation not in action.precond @@ -255,7 +266,9 @@ def implication(a: Var, b: Var): @staticmethod def _step2I( - obs_lists: ObservationLists, relations: dict + obs_lists: ObservationLists, + relations: Dict[Fluent, Relation], + actions: Dict[Action, LearnedAction], ) -> Tuple[List[Or[Var]], Dict[Or[Var], int]]: constraints: List[Or[Var]] = [] support_counts: Dict[Or[Var], int] = defaultdict(int) @@ -269,7 +282,7 @@ def _step2I( # relation in the add list of an action <= n (i-1) i1: List[Var] = [] for obs_i in obs_list[: i - 1]: - ai = obs_i.action + ai = actions[obs_i.action] i1.append( Var( f"{relations[fluent].var()}_in_add_{ai.details()}" @@ -279,7 +292,7 @@ def _step2I( # I2 # relation not in del list of action n (i-1) i2 = Var( - f"{relations[fluent].var()}_in_del_{obs_list[i-1].action.details()}" + f"{relations[fluent].var()}_in_del_{actions[obs_list[i-1].action].details()}" ).negate() constraints.append(Or(i1)) @@ -293,7 +306,7 @@ def _step2I( Or( [ Var( - f"{relations[fluent].var()}_in_pre_{obs.action.details()}" + f"{relations[fluent].var()}_in_pre_{actions[obs.action].details()}" ) ] ) @@ -304,7 +317,7 @@ def _step2I( Or( [ Var( - f"{relations[fluent].var()}_in_add_{obs_list[i-1].action.details()}" + f"{relations[fluent].var()}_in_add_{actions[obs_list[i-1].action].details()}" ) ] ) @@ -511,17 +524,55 @@ def _step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: return model @staticmethod - def _step5(model: Dict[Hashable, bool], actions: List[LearnedAction]): + def _step5( + model: Dict[Hashable, bool], + actions: List[LearnedAction], + relations: List[Relation], + ): action_map = {a.details(): a for a in actions} - print(actions[0].details()) + relation_map = {p.var(): p for p in relations} + for constraint, val in model.items(): constraint = str(constraint).split("_") - print(constraint) + print(constraint, val) + fluent = relation_map[constraint[0]] relation = constraint[0] ctype = constraint[1] # constraint type if ctype == "in": - alist = constraint[2] - action = constraint[3] + effect = constraint[2] + action = action_map[constraint[3]] + if val: + action_update = ( + action.update_precond + if effect == "pre" + else action.update_add + if effect == "add" + else action.update_delete + ) + # action_update({str(fluent)}) + action_update({relation}) + else: + action_effect = ( + action.precond + if effect == "pre" + else action.add + if effect == "add" + else action.delete + ) + if fluent in action_effect: + # TODO: determine if this is an error, or if it just + # means the effect should be removed (due to more info + # from later iterations) + raise ConstraintContradiction(fluent, effect, action) else: + # plan constraint + # doesn't directly affect actions a1 = constraint[2] a2 = constraint[3] + + for action in action_map.values(): + print() + print(action.details()) + print("precond:", action.precond) + print("add:", action.add) + print("delete:", action.delete) From 114e9afae574bfbeea57723945a5df809b19883f Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 28 Jul 2021 15:41:08 -0400 Subject: [PATCH 056/181] Add upper bound --- macq/extract/arms.py | 60 +++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index ebe73278..04fb1502 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -54,6 +54,8 @@ def __init__(self, threshold): def __new__( cls, obs_lists: ObservationLists, + debug: bool, + upper_bound: int, min_support: int = 2, action_weight: int = 110, info_weight: int = 100, @@ -65,6 +67,9 @@ def __new__( Arguments: obs_lists (ObservationLists): The observations to extract the model from. + upper_bound (int): + The upper bound for the maximum size of an action's preconditions and + add/delete lists. Determines when an action schemata is fully learned. min_support (int): The minimum support count for an action pair to be considered frequent. action_weight (int): @@ -86,10 +91,12 @@ def __new__( raise ARMS.InvalidThreshold(threshold) # get fluents from initial state - fluents = ARMS._get_fluents(obs_lists) + fluents = obs_lists.get_fluents() # call algorithm to get actions actions = ARMS._arms( obs_lists, + debug, + upper_bound, fluents, min_support, action_weight, @@ -100,14 +107,11 @@ def __new__( ) return Model(fluents, actions) - @staticmethod - def _get_fluents(obs_lists: ObservationLists) -> Set[Fluent]: - """Retrieves the set of fluents in the observations.""" - return obs_lists.get_fluents() - @staticmethod def _arms( obs_lists: ObservationLists, + debug: bool, + upper_bound: int, fluents: Set[Fluent], min_support: int, action_weight: int, @@ -117,11 +121,13 @@ def _arms( plan_default: int, ) -> Set[LearnedAction]: """The main driver for the ARMS algorithm.""" + learned_actions = set() - connected_actions, learned_actions = ARMS._step1(obs_lists) + connected_actions, actions = ARMS._step1(obs_lists) + actions_rev = {l: a for a, l in actions.items()} constraints, relations = ARMS._step2( - obs_lists, connected_actions, learned_actions, fluents, min_support + obs_lists, connected_actions, actions, fluents, min_support ) max_sat, decode = ARMS._step3( @@ -135,10 +141,35 @@ def _arms( model = ARMS._step4(max_sat, decode) - action_models = ARMS._step5( - model, list(learned_actions.values()), list(relations.values()) - ) - return set() # WARNING temp + # actions mutated in place (don't need to return) + ARMS._step5(model, list(actions.values()), list(relations.values())) + + for action in actions.values(): + print(action.details()) + + setA = set() + for action in actions.values(): + if debug: + ARMS.debug(action=action) + if ( + max([len(action.precond), len(action.add), len(action.delete)]) + >= upper_bound + ): + if debug: + print( + f"Action schemata for {action.details()} has been fully learned." + ) + setA.add(action) + + for action in setA: + action_key = actions_rev[action] + del actions[action_key] + del action_key + learned_actions.add(action) + + # TODO + # return set(learned_actions.values()) + return set() @staticmethod def _step1( @@ -534,7 +565,6 @@ def _step5( for constraint, val in model.items(): constraint = str(constraint).split("_") - print(constraint, val) fluent = relation_map[constraint[0]] relation = constraint[0] ctype = constraint[1] # constraint type @@ -570,7 +600,9 @@ def _step5( a1 = constraint[2] a2 = constraint[3] - for action in action_map.values(): + @staticmethod + def debug(action=None): + if action: print() print(action.details()) print("precond:", action.precond) From b15adec688ae53d6b927bc6325c6386134a66ef5 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 28 Jul 2021 15:41:28 -0400 Subject: [PATCH 057/181] Add debug arg to Extract API --- macq/extract/extract.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/macq/extract/extract.py b/macq/extract/extract.py index 118739ff..8ad2d72a 100644 --- a/macq/extract/extract.py +++ b/macq/extract/extract.py @@ -35,7 +35,9 @@ class Extract: from state observations. """ - def __new__(cls, obs_lists: ObservationLists, mode: modes, **kwargs) -> Model: + def __new__( + cls, obs_lists: ObservationLists, mode: modes, debug: bool = False, **kwargs + ) -> Model: """Extracts a Model object. Extracts a model from the observations using the specified extraction @@ -62,4 +64,4 @@ def __new__(cls, obs_lists: ObservationLists, mode: modes, **kwargs) -> Model: if len(obs_lists) != 1: raise Exception("The SLAF extraction technique only takes one trace.") - return techniques[mode](obs_lists, **kwargs) + return techniques[mode](obs_lists, debug, **kwargs) From 0101fc917059af22b75d43e4d1b68a0532811d2d Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 28 Jul 2021 16:06:48 -0400 Subject: [PATCH 058/181] Use reversed action map for learning actions --- macq/extract/arms.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 04fb1502..76712adb 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -123,11 +123,14 @@ def _arms( """The main driver for the ARMS algorithm.""" learned_actions = set() - connected_actions, actions = ARMS._step1(obs_lists) - actions_rev = {l: a for a, l in actions.items()} + connected_actions, action_map = ARMS._step1(obs_lists) + + action_map_rev: Dict[LearnedAction, List[Action]] = defaultdict(list) + for obs_action, learned_action in action_map.items(): + action_map_rev[learned_action].append(obs_action) constraints, relations = ARMS._step2( - obs_lists, connected_actions, actions, fluents, min_support + obs_lists, connected_actions, action_map, fluents, min_support ) max_sat, decode = ARMS._step3( @@ -142,13 +145,10 @@ def _arms( model = ARMS._step4(max_sat, decode) # actions mutated in place (don't need to return) - ARMS._step5(model, list(actions.values()), list(relations.values())) - - for action in actions.values(): - print(action.details()) + ARMS._step5(model, list(action_map.values()), list(relations.values())) setA = set() - for action in actions.values(): + for action in action_map.values(): if debug: ARMS.debug(action=action) if ( @@ -162,9 +162,9 @@ def _arms( setA.add(action) for action in setA: - action_key = actions_rev[action] - del actions[action_key] - del action_key + action_keys = action_map_rev[action] + for a in action_keys: + del action_map[a] learned_actions.add(action) # TODO From 4c82d9011c9f58c37f1d53b57ee10c07897e70cd Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 29 Jul 2021 10:55:55 -0400 Subject: [PATCH 059/181] Fix missing message --- macq/extract/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macq/extract/exceptions.py b/macq/extract/exceptions.py index ac51fa01..c6a473aa 100644 --- a/macq/extract/exceptions.py +++ b/macq/extract/exceptions.py @@ -9,7 +9,7 @@ class InconsistentConstraintWeights(Exception): def __init__(self, constraint, weight1, weight2, message=None): if message is None: message = f"Tried to assign the constraint {constraint} conflicting weights ({weight1} and {weight2})." - super().__init__() + super().__init__(message) class InvalidMaxSATModel(Exception): From 17b5399ef792e40f42e10c14b9f87322064f263a Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 29 Jul 2021 10:56:16 -0400 Subject: [PATCH 060/181] Add typing to LearnedAction obj_params --- macq/extract/learned_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macq/extract/learned_action.py b/macq/extract/learned_action.py index 5bf260a4..fd928e5e 100644 --- a/macq/extract/learned_action.py +++ b/macq/extract/learned_action.py @@ -3,7 +3,7 @@ class LearnedAction: - def __init__(self, name: str, obj_params: Set, **kwargs): + def __init__(self, name: str, obj_params: Set[str], **kwargs): self.name = name self.obj_params = obj_params if "cost" in kwargs: From 94ea2ee89fabb0f6ba2229142b915a1c90c8f5f3 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 29 Jul 2021 10:56:29 -0400 Subject: [PATCH 061/181] Add main ARMS loop --- macq/extract/arms.py | 124 ++++++++++++++++++++++++------------------- 1 file changed, 70 insertions(+), 54 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 76712adb..ea84dc86 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -33,7 +33,6 @@ class ARMSConstraints: action: List[Or[Var]] info: List[Or[Var]] info3: Dict[Or[Var], int] - # plan: Dict[And[Or[Var]], int] plan: Dict[Or[Var], int] @@ -129,43 +128,55 @@ def _arms( for obs_action, learned_action in action_map.items(): action_map_rev[learned_action].append(obs_action) - constraints, relations = ARMS._step2( - obs_lists, connected_actions, action_map, fluents, min_support - ) + while action_map_rev: + constraints, relations = ARMS._step2( + obs_lists, + connected_actions, + action_map, + fluents, + min_support, + ) - max_sat, decode = ARMS._step3( - constraints, - action_weight, - info_weight, - threshold, - info3_default, - plan_default, - ) + max_sat, decode = ARMS._step3( + constraints, + action_weight, + info_weight, + threshold, + info3_default, + plan_default, + ) - model = ARMS._step4(max_sat, decode) + model = ARMS._step4(max_sat, decode) - # actions mutated in place (don't need to return) - ARMS._step5(model, list(action_map.values()), list(relations.values())) + # actions mutated in place (don't need to return) + ARMS._step5(model, list(action_map_rev.keys()), list(relations.values())) - setA = set() - for action in action_map.values(): - if debug: - ARMS.debug(action=action) - if ( - max([len(action.precond), len(action.add), len(action.delete)]) - >= upper_bound - ): + setA = set() + for action in action_map_rev.keys(): if debug: - print( - f"Action schemata for {action.details()} has been fully learned." - ) - setA.add(action) - - for action in setA: - action_keys = action_map_rev[action] - for a in action_keys: - del action_map[a] - learned_actions.add(action) + ARMS.debug(action=action) + if ( + max([len(action.precond), len(action.add), len(action.delete)]) + >= upper_bound + ): + if debug: + print( + f"Action schemata for {action.details()} has been fully learned." + ) + setA.add(action) + + for action in setA: + action_keys = action_map_rev[action] + for obs_action in action_keys: + del action_map[obs_action] + del action_map_rev[action] + del connected_actions[action] + action_keys = [ + a1 for a1 in connected_actions if action in connected_actions[a1] + ] + for a1 in action_keys: + del connected_actions[a1][action] + learned_actions.add(action) # TODO # return set(learned_actions.values()) @@ -175,7 +186,7 @@ def _arms( def _step1( obs_lists: ObservationLists, ) -> Tuple[ - Dict[LearnedAction, Dict[LearnedAction, Set]], + Dict[LearnedAction, Dict[LearnedAction, Set[str]]], Dict[Action, LearnedAction], ]: """Substitute instantiated objects in each action instance with the object type.""" @@ -193,7 +204,7 @@ def _step1( actions.append(action) learned_actions[obs_action] = action - connected_actions = {} + connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set[str]]] = {} for i, a1 in enumerate(actions): connected_actions[a1] = {} for a2 in actions[i:]: # includes connecting with self @@ -207,7 +218,7 @@ def _step1( def _step2( obs_lists: ObservationLists, connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], - learned_actions: Dict[Action, LearnedAction], + action_map: Dict[Action, LearnedAction], fluents: Set[Fluent], min_support: int, ) -> Tuple[ARMSConstraints, Dict[Fluent, Relation]]: @@ -230,13 +241,13 @@ def _step2( action_constraints = ARMS._step2A(connected_actions, set(relations.values())) info_constraints, info_support_counts = ARMS._step2I( - obs_lists, relations, learned_actions + obs_lists, relations, action_map ) plan_constraints = ARMS._step2P( obs_lists, connected_actions, - learned_actions, + action_map, set(relations.values()), min_support, ) @@ -313,25 +324,30 @@ def _step2I( # relation in the add list of an action <= n (i-1) i1: List[Var] = [] for obs_i in obs_list[: i - 1]: - ai = actions[obs_i.action] - i1.append( - Var( - f"{relations[fluent].var()}_in_add_{ai.details()}" + if obs_i.action in actions: + ai = actions[obs_i.action] + i1.append( + Var( + f"{relations[fluent].var()}_in_add_{ai.details()}" + ) ) - ) # I2 # relation not in del list of action n (i-1) - i2 = Var( - f"{relations[fluent].var()}_in_del_{actions[obs_list[i-1].action].details()}" - ).negate() + i2 = None + if obs_list[i - 1].action in actions: + i2 = Var( + f"{relations[fluent].var()}_in_del_{actions[obs_list[i-1].action].details()}" + ).negate() - constraints.append(Or(i1)) - constraints.append(Or([i2])) + if i1: + constraints.append(Or(i1)) + if i2: + constraints.append(Or([i2])) # I3 # count occurences - if i < len(obs_list) - 1: + if i < len(obs_list) - 1 and obs.action in actions: # corresponding constraint is related to the current action's precondition list support_counts[ Or( @@ -342,7 +358,7 @@ def _step2I( ] ) ] += 1 - else: + elif obs_list[i - 1].action in actions: # corresponding constraint is related to the previous action's add list support_counts[ Or( @@ -402,7 +418,7 @@ def _apriori( def _step2P( obs_lists: ObservationLists, connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], - learned_actions: Dict[Action, LearnedAction], + action_map: Dict[Action, LearnedAction], relations: Set[Relation], min_support: int, ) -> Dict[Or[Var], int]: @@ -410,9 +426,9 @@ def _step2P( frequent_pairs = ARMS._apriori( [ [ - learned_actions[obs.action] + action_map[obs.action] for obs in obs_list - if obs.action is not None + if obs.action is not None and obs.action in action_map ] for obs_list in obs_lists ], @@ -563,7 +579,7 @@ def _step5( action_map = {a.details(): a for a in actions} relation_map = {p.var(): p for p in relations} - for constraint, val in model.items(): + for constraint, val in list(model.items())[:50]: constraint = str(constraint).split("_") fluent = relation_map[constraint[0]] relation = constraint[0] From 55ce324cd5481bed99541eb2e00f722cb7a8a0f0 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 29 Jul 2021 13:58:13 -0400 Subject: [PATCH 062/181] Fix get_fluents removed from ObservationLists --- macq/trace/observation_lists.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index 3286e862..3d454c66 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -1,4 +1,4 @@ -from typing import List, Type, Set +from typing import Iterator, List, Type, Set from . import Trace from ..observation import Observation import macq.trace as TraceAPI @@ -10,7 +10,6 @@ class ObservationLists(TraceAPI.TraceList): generate_more = property() get_usage = property() tokenize = property() - get_fluents = property() def __init__(self, traces: TraceAPI.TraceList, Token: Type[Observation], **kwargs): self.traces = [] From 53228f927281fae5bcc46f240d80e9972f8a3aba Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 29 Jul 2021 13:59:25 -0400 Subject: [PATCH 063/181] Add more debugging functionalty --- macq/extract/arms.py | 63 ++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index ea84dc86..061ac586 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -94,7 +94,6 @@ def __new__( # call algorithm to get actions actions = ARMS._arms( obs_lists, - debug, upper_bound, fluents, min_support, @@ -103,13 +102,13 @@ def __new__( threshold, info3_default, plan_default, + debug, ) return Model(fluents, actions) @staticmethod def _arms( obs_lists: ObservationLists, - debug: bool, upper_bound: int, fluents: Set[Fluent], min_support: int, @@ -118,11 +117,12 @@ def _arms( threshold: float, info3_default: int, plan_default: int, + debug: bool, ) -> Set[LearnedAction]: """The main driver for the ARMS algorithm.""" learned_actions = set() - connected_actions, action_map = ARMS._step1(obs_lists) + connected_actions, action_map = ARMS._step1(obs_lists, debug) action_map_rev: Dict[LearnedAction, List[Action]] = defaultdict(list) for obs_action, learned_action in action_map.items(): @@ -130,11 +130,7 @@ def _arms( while action_map_rev: constraints, relations = ARMS._step2( - obs_lists, - connected_actions, - action_map, - fluents, - min_support, + obs_lists, connected_actions, action_map, fluents, min_support, debug ) max_sat, decode = ARMS._step3( @@ -144,12 +140,15 @@ def _arms( threshold, info3_default, plan_default, + debug, ) - model = ARMS._step4(max_sat, decode) + model = ARMS._step4(max_sat, decode, debug) # actions mutated in place (don't need to return) - ARMS._step5(model, list(action_map_rev.keys()), list(relations.values())) + ARMS._step5( + model, list(action_map_rev.keys()), list(relations.values()), debug + ) setA = set() for action in action_map_rev.keys(): @@ -184,7 +183,7 @@ def _arms( @staticmethod def _step1( - obs_lists: ObservationLists, + obs_lists: ObservationLists, debug: bool ) -> Tuple[ Dict[LearnedAction, Dict[LearnedAction, Set[str]]], Dict[Action, LearnedAction], @@ -221,6 +220,7 @@ def _step2( action_map: Dict[Action, LearnedAction], fluents: Set[Fluent], min_support: int, + debug: bool, ) -> Tuple[ARMSConstraints, Dict[Fluent, Relation]]: """Generate action constraints, information constraints, and plan constraints.""" @@ -239,9 +239,11 @@ def _step2( ) ) - action_constraints = ARMS._step2A(connected_actions, set(relations.values())) + action_constraints = ARMS._step2A( + connected_actions, set(relations.values()), debug + ) info_constraints, info_support_counts = ARMS._step2I( - obs_lists, relations, action_map + obs_lists, relations, action_map, debug ) plan_constraints = ARMS._step2P( @@ -266,7 +268,12 @@ def _step2( def _step2A( connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], relations: Set[Relation], + debug: bool, ) -> List[Or[Var]]: + + if debug: + print("Building action constraints...\n") + def implication(a: Var, b: Var): return Or([a.negate(), b]) @@ -275,13 +282,21 @@ def implication(a: Var, b: Var): for action in actions: for relation in relations: # A relation is relevant to an action if they share parameter types - if set(relation.types).issubset(action.obj_params): + if relation.types and set(relation.types).issubset(action.obj_params): + if debug: + print( + f'relation ({relation.var()}) is relevant to action "{action.details()}"\n' + "A1:\n" + f" {relation.var()} ∈ add ⇒ {relation.var()} ∉ pre\n" + f" {relation.var()} ∈ pre ⇒ {relation.var()} ∉ add\n" + "A2:\n" + f" {relation.var()} ∈ del ⇒ {relation.var()} ∈ pre\n" + ) # A1 - # relation in action.add <=> relation not in action.precond + # relation in action.add => relation not in action.precond + # relation in action.precond => relation not in action.add - # _ is used to mark split locations for parsing later. - # Can't use spaces because both relation.var and - # action.details() contain spaces. + # _ is used to unambiguously mark split locations for parsing later. constraints.append( implication( Var(f"{relation.var()}_in_add_{action.details()}"), @@ -311,12 +326,18 @@ def _step2I( obs_lists: ObservationLists, relations: Dict[Fluent, Relation], actions: Dict[Action, LearnedAction], + debug: bool, ) -> Tuple[List[Or[Var]], Dict[Or[Var], int]]: constraints: List[Or[Var]] = [] support_counts: Dict[Or[Var], int] = defaultdict(int) - for obs_list in obs_lists: + obs_list: List[Observation] + for obs_list_i, obs_list in enumerate(obs_lists): for i, obs in enumerate(obs_list): if obs.state is not None and i > 0: + if debug: + print( + f"State {i+1} of observation list {obs_list_i+1} has information." + ) for fluent, val in obs.state.items(): # Information constraints only apply to true relations if val: @@ -324,7 +345,9 @@ def _step2I( # relation in the add list of an action <= n (i-1) i1: List[Var] = [] for obs_i in obs_list[: i - 1]: - if obs_i.action in actions: + # action will never be None if it's in actions, + # but the condition is needed to make linting happy + if obs_i.action in actions and obs_i.action is not None: ai = actions[obs_i.action] i1.append( Var( From 689fda1fd10afd0721adbbf5ca90d3eb8e228f55 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 29 Jul 2021 14:19:07 -0400 Subject: [PATCH 064/181] Fix possible missing action in info constraints --- macq/extract/arms.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 061ac586..bd4d6c00 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -358,9 +358,10 @@ def _step2I( # I2 # relation not in del list of action n (i-1) i2 = None - if obs_list[i - 1].action in actions: + a_n = obs_list[i - 1].action + if a_n in actions and a_n is not None: i2 = Var( - f"{relations[fluent].var()}_in_del_{actions[obs_list[i-1].action].details()}" + f"{relations[fluent].var()}_in_del_{actions[a_n].details()}" ).negate() if i1: @@ -370,7 +371,11 @@ def _step2I( # I3 # count occurences - if i < len(obs_list) - 1 and obs.action in actions: + if ( + i < len(obs_list) - 1 + and obs.action in actions + and obs.action is not None # for the linter + ): # corresponding constraint is related to the current action's precondition list support_counts[ Or( @@ -381,13 +386,13 @@ def _step2I( ] ) ] += 1 - elif obs_list[i - 1].action in actions: + elif a_n in actions and a_n is not None: # corresponding constraint is related to the previous action's add list support_counts[ Or( [ Var( - f"{relations[fluent].var()}_in_add_{actions[obs_list[i-1].action].details()}" + f"{relations[fluent].var()}_in_add_{actions[a_n].details()}" ) ] ) @@ -523,6 +528,7 @@ def _step3( threshold: float, info3_default: int, plan_default: int, + debug: bool, ) -> Tuple[WCNF, Dict[int, Hashable]]: """Construct the weighted MAX-SAT problem.""" @@ -579,7 +585,9 @@ def get_support_rate(count): return list(map(get_support_rate, support_counts)) @staticmethod - def _step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: + def _step4( + max_sat: WCNF, decode: Dict[int, Hashable], debug: bool + ) -> Dict[Hashable, bool]: solver = RC2(max_sat) solver.compute() encoded_model = solver.model @@ -598,6 +606,7 @@ def _step5( model: Dict[Hashable, bool], actions: List[LearnedAction], relations: List[Relation], + debug: bool, ): action_map = {a.details(): a for a in actions} relation_map = {p.var(): p for p in relations} From 268eb30105501db11c0c32754aee6eaf17828599 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 29 Jul 2021 15:49:39 -0400 Subject: [PATCH 065/181] More verbose debugging info --- macq/extract/arms.py | 53 ++++++++++++++++++++++----------- macq/trace/observation_lists.py | 2 +- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index bd4d6c00..31550e2c 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,5 +1,6 @@ from collections import defaultdict, Counter from dataclasses import dataclass +from logging import warn from typing import Set, List, Dict, Tuple, Hashable from nnf import NNF, Var, And, Or, false as nnffalse from pysat.examples.rc2 import RC2 @@ -190,8 +191,8 @@ def _step1( ]: """Substitute instantiated objects in each action instance with the object type.""" - actions: List[LearnedAction] = [] - learned_actions = {} + learned_actions: Set[LearnedAction] = set() + action_map: Dict[Action, LearnedAction] = {} for obs_action in obs_lists.get_actions(): # We don't support objects with multiple types right now, so no # multiple type clauses need to be generated. @@ -199,19 +200,19 @@ def _step1( # Create LearnedActions for each action, replacing instantiated # objects with the object type. types = {obj.obj_type for obj in obs_action.obj_params} - action = LearnedAction(obs_action.name, types) - actions.append(action) - learned_actions[obs_action] = action + learned_action = LearnedAction(obs_action.name, types) + learned_actions.add(learned_action) + action_map[obs_action] = learned_action connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set[str]]] = {} - for i, a1 in enumerate(actions): + for i, a1 in enumerate(learned_actions): connected_actions[a1] = {} - for a2 in actions[i:]: # includes connecting with self + for a2 in learned_actions.difference({a1}): # includes connecting with self intersection = a1.obj_params.intersection(a2.obj_params) if intersection: connected_actions[a1][a2] = intersection - return connected_actions, learned_actions + return connected_actions, action_map @staticmethod def _step2( @@ -242,6 +243,7 @@ def _step2( action_constraints = ARMS._step2A( connected_actions, set(relations.values()), debug ) + info_constraints, info_support_counts = ARMS._step2I( obs_lists, relations, action_map, debug ) @@ -272,7 +274,7 @@ def _step2A( ) -> List[Or[Var]]: if debug: - print("Building action constraints...\n") + print("\nBuilding action constraints...\n") def implication(a: Var, b: Var): return Or([a.negate(), b]) @@ -287,16 +289,18 @@ def implication(a: Var, b: Var): print( f'relation ({relation.var()}) is relevant to action "{action.details()}"\n' "A1:\n" - f" {relation.var()} ∈ add ⇒ {relation.var()} ∉ pre\n" - f" {relation.var()} ∈ pre ⇒ {relation.var()} ∉ add\n" + f" {relation.var()}∈ add ⇒ {relation.var()}∉ pre\n" + f" {relation.var()}∈ pre ⇒ {relation.var()}∉ add\n" "A2:\n" - f" {relation.var()} ∈ del ⇒ {relation.var()} ∈ pre\n" + f" {relation.var()}∈ del ⇒ {relation.var()}∈ pre\n" ) + # A1 # relation in action.add => relation not in action.precond # relation in action.precond => relation not in action.add - # _ is used to unambiguously mark split locations for parsing later. + # underscores are used to unambiguously mark split locations + # for parsing constraints later. constraints.append( implication( Var(f"{relation.var()}_in_add_{action.details()}"), @@ -328,19 +332,27 @@ def _step2I( actions: Dict[Action, LearnedAction], debug: bool, ) -> Tuple[List[Or[Var]], Dict[Or[Var], int]]: + + if debug: + print("\nBuilding information constraints...") constraints: List[Or[Var]] = [] support_counts: Dict[Or[Var], int] = defaultdict(int) obs_list: List[Observation] for obs_list_i, obs_list in enumerate(obs_lists): for i, obs in enumerate(obs_list): if obs.state is not None and i > 0: + n = i - 1 if debug: print( - f"State {i+1} of observation list {obs_list_i+1} has information." + f"\nStep {i} of observation list {obs_list_i} contains state information." ) for fluent, val in obs.state.items(): # Information constraints only apply to true relations if val: + print( + f" Fluent {fluent} is true.\n" + f" ({relations[fluent].var()})∈ ({' ∪ '.join([f'add_{{ {actions[obs_list[ik].action].details()} }}' for ik in range(0,n+1) if obs_list[ik].action in actions] )})" # type: ignore + ) # I1 # relation in the add list of an action <= n (i-1) i1: List[Var] = [] @@ -555,9 +567,16 @@ def _step3( if constraint not in constraints_w_weights: constraints_w_weights[constraint] = weight elif weight != constraints_w_weights[constraint]: - raise InconsistentConstraintWeights( - constraint, weight, constraints_w_weights[constraint] + warn( + f"The constraint {constraint} has conflicting weights ({weight} and {constraints_w_weights[constraint]}). Choosing the smaller weight." ) + constraints_w_weights[constraint] = min( + weight, constraints_w_weights[constraint] + ) + + # raise InconsistentConstraintWeights( + # constraint, weight, constraints_w_weights[constraint] + # ) problem: And[Or[Var]] = And(list(constraints_w_weights.keys())) weights = list(constraints_w_weights.values()) @@ -611,7 +630,7 @@ def _step5( action_map = {a.details(): a for a in actions} relation_map = {p.var(): p for p in relations} - for constraint, val in list(model.items())[:50]: + for constraint, val in list(model.items())[:25]: constraint = str(constraint).split("_") fluent = relation_map[constraint[0]] relation = constraint[0] diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index 3d454c66..6709e242 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -24,7 +24,7 @@ def get_actions(self): for obs_list in self: for obs in obs_list: action = obs.action - if action: + if action is not None: actions.add(action) return actions From 5d3bd67771dd394efe7e7a38b771b9e5fa082b01 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 29 Jul 2021 16:48:01 -0400 Subject: [PATCH 066/181] Clean up debugging output --- macq/extract/arms.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 31550e2c..f5020c78 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -151,8 +151,10 @@ def _arms( model, list(action_map_rev.keys()), list(relations.values()), debug ) + # Step 5 updates setA = set() for action in action_map_rev.keys(): + # check if actions need to be learned in order if debug: ARMS.debug(action=action) if ( @@ -166,6 +168,7 @@ def _arms( setA.add(action) for action in setA: + # advance early states action_keys = action_map_rev[action] for obs_action in action_keys: del action_map[obs_action] @@ -205,7 +208,7 @@ def _step1( action_map[obs_action] = learned_action connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set[str]]] = {} - for i, a1 in enumerate(learned_actions): + for a1 in learned_actions: connected_actions[a1] = {} for a2 in learned_actions.difference({a1}): # includes connecting with self intersection = a1.obj_params.intersection(a2.obj_params) @@ -351,7 +354,9 @@ def _step2I( if val: print( f" Fluent {fluent} is true.\n" - f" ({relations[fluent].var()})∈ ({' ∪ '.join([f'add_{{ {actions[obs_list[ik].action].details()} }}' for ik in range(0,n+1) if obs_list[ik].action in actions] )})" # type: ignore + f" ({relations[fluent].var()})∈ (" + f"{' ∪ '.join([f'add_{{ {actions[obs_list[ik].action].details()} }}' for ik in range(0,n+1) if obs_list[ik].action in actions] )}" # type: ignore + ")" ) # I1 # relation in the add list of an action <= n (i-1) From 2de59b0667e4e71b4e6b4e49d8bcb4ff89c17c76 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 30 Jul 2021 16:03:23 -0400 Subject: [PATCH 067/181] Add early action pointers and early state update --- macq/extract/arms.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index f5020c78..ada61f44 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -125,6 +125,8 @@ def _arms( connected_actions, action_map = ARMS._step1(obs_lists, debug) + early_actions = [0] * len(obs_lists) + action_map_rev: Dict[LearnedAction, List[Action]] = defaultdict(list) for obs_action, learned_action in action_map.items(): action_map_rev[learned_action].append(obs_action) @@ -152,11 +154,21 @@ def _arms( ) # Step 5 updates + # makes more sense to perform the updates in this function context setA = set() for action in action_map_rev.keys(): # check if actions need to be learned in order + for i, obs_list in enumerate(obs_lists): + # if complete action is the early action for obs_list i + if action == obs_list[early_actions[i]]: + for add in action.add: + print(add) + # make add effects true in state + # make del effects false + if debug: ARMS.debug(action=action) + if ( max([len(action.precond), len(action.add), len(action.delete)]) >= upper_bound @@ -352,12 +364,13 @@ def _step2I( for fluent, val in obs.state.items(): # Information constraints only apply to true relations if val: - print( - f" Fluent {fluent} is true.\n" - f" ({relations[fluent].var()})∈ (" - f"{' ∪ '.join([f'add_{{ {actions[obs_list[ik].action].details()} }}' for ik in range(0,n+1) if obs_list[ik].action in actions] )}" # type: ignore - ")" - ) + if debug: + print( + f" Fluent {fluent} is true.\n" + f" ({relations[fluent].var()})∈ (" + f"{' ∪ '.join([f'add_{{ {actions[obs_list[ik].action].details()} }}' for ik in range(0,n+1) if obs_list[ik].action in actions] )}" # type: ignore + ")" + ) # I1 # relation in the add list of an action <= n (i-1) i1: List[Var] = [] From 88f6e60c2c5a7b615bfd49e9a180a22c833dbb00 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 3 Aug 2021 17:18:08 -0400 Subject: [PATCH 068/181] Fix intermediate observation state updates --- macq/extract/arms.py | 40 ++++++++++++----- macq/extract/learned_fluent.py | 1 + macq/extract/model.py | 14 +++--- macq/extract/slaf.py | 4 +- macq/generate/pddl/generator.py | 13 +++--- macq/generate/pddl/random_goal_sampling.py | 45 ++++++++++--------- macq/generate/pddl/vanilla_sampling.py | 10 +++-- macq/generate/plan.py | 4 +- macq/utils/__init__.py | 12 ++++- macq/utils/common_errors.py | 2 +- tests/extract/test_observer.py | 12 +++-- tests/extract/test_slaf.py | 12 +++-- tests/generate/pddl/test_plan.py | 2 +- .../pddl/test_random_goal_sampling.py | 3 +- tests/generate/pddl/test_trace_from_goal.py | 8 +++- tests/generate/pddl/test_vanilla_sampling.py | 9 ++-- 16 files changed, 126 insertions(+), 65 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index ada61f44..33b0f5d1 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -132,10 +132,14 @@ def _arms( action_map_rev[learned_action].append(obs_action) while action_map_rev: - constraints, relations = ARMS._step2( + constraints, relation_map = ARMS._step2( obs_lists, connected_actions, action_map, fluents, min_support, debug ) + relation_map_rev: Dict[Relation, List[Fluent]] = defaultdict(list) + for fluent, relation in relation_map.items(): + relation_map_rev[relation].append(fluent) + max_sat, decode = ARMS._step3( constraints, action_weight, @@ -148,23 +152,36 @@ def _arms( model = ARMS._step4(max_sat, decode, debug) - # actions mutated in place (don't need to return) + # Mutates the LearnedAction (keys) of action_map_rev ARMS._step5( - model, list(action_map_rev.keys()), list(relations.values()), debug + model, list(action_map_rev.keys()), list(relation_map.values()), debug ) # Step 5 updates - # makes more sense to perform the updates in this function context setA = set() for action in action_map_rev.keys(): - # check if actions need to be learned in order for i, obs_list in enumerate(obs_lists): - # if complete action is the early action for obs_list i - if action == obs_list[early_actions[i]]: + obs_action: Action = obs_list[early_actions[i]].action + # if current action is the early action for obs_list i, + # update the next state with the effects and update the + # early action pointer + if obs_action in action_map and action == action_map[obs_action]: + # Set add effects true for add in action.add: - print(add) - # make add effects true in state - # make del effects false + # get candidate fluents from add relation + # get fluent by cross referencing obs_list.action params + candidates = relation_map_rev[add] + for fluent in candidates: + if set(fluent.objects).issubset(obs_action.obj_params): + obs_list[early_actions[i] + 1].state[fluent] = True + early_actions[i] += 1 + # Set del effects false + for delete in action.delete: + candidates = relation_map_rev[delete] + for fluent in candidates: + if set(fluent.objects).issubset(obs_action.obj_params): + obs_list[early_actions[i] + 1].state[fluent] = False + early_actions[i] += 1 if debug: ARMS.debug(action=action) @@ -679,8 +696,7 @@ def _step5( # means the effect should be removed (due to more info # from later iterations) raise ConstraintContradiction(fluent, effect, action) - else: - # plan constraint + else: # plan constraint # doesn't directly affect actions a1 = constraint[2] a2 = constraint[3] diff --git a/macq/extract/learned_fluent.py b/macq/extract/learned_fluent.py index c80d3545..6955f5bf 100644 --- a/macq/extract/learned_fluent.py +++ b/macq/extract/learned_fluent.py @@ -1,5 +1,6 @@ from typing import List + class LearnedFluent: def __init__(self, name: str, objects: List): self.name = name diff --git a/macq/extract/model.py b/macq/extract/model.py index 6a2fc578..04af2c05 100644 --- a/macq/extract/model.py +++ b/macq/extract/model.py @@ -27,9 +27,7 @@ class Model: action attributes characterize the model. """ - def __init__( - self, fluents: Set[LearnedFluent], actions: Set[LearnedAction] - ): + def __init__(self, fluents: Set[LearnedFluent], actions: Set[LearnedAction]): """Initializes a Model with a set of fluents and a set of actions. Args: @@ -129,7 +127,13 @@ def __to_tarski_formula(self, attribute: Set[str], lang: FirstOrderLanguage): Connective.And, [lang.get(a.replace(" ", "_"))() for a in attribute] ) - def to_pddl(self, domain_name: str, problem_name: str, domain_filename: str, problem_filename: str): + def to_pddl( + self, + domain_name: str, + problem_name: str, + domain_filename: str, + problem_filename: str, + ): """Dumps a Model to two PDDL files. The conversion only uses 0-arity predicates, and no types, objects, or parameters of any kind are used. Actions are represented as ground actions with no parameters. @@ -192,4 +196,4 @@ def deserialize(string: str): @classmethod def _from_json(cls, data: dict): actions = set(map(LearnedAction._deserialize, data["actions"])) - return cls(set(data["fluents"]), actions) \ No newline at end of file + return cls(set(data["fluents"]), actions) diff --git a/macq/extract/slaf.py b/macq/extract/slaf.py index ada95794..72254df5 100644 --- a/macq/extract/slaf.py +++ b/macq/extract/slaf.py @@ -167,7 +167,9 @@ def __sort_results(observations: ObservationLists, entailed: Set): # iterate through each step for o in observations: for token in o: - model_fluents.update([LearnedFluent(name=f, objects=[]) for f in token.state]) + model_fluents.update( + [LearnedFluent(name=f, objects=[]) for f in token.state] + ) # if an action was taken on this step if token.action: # set up a base LearnedAction with the known information diff --git a/macq/generate/pddl/generator.py b/macq/generate/pddl/generator.py index adb3c218..20f6f359 100644 --- a/macq/generate/pddl/generator.py +++ b/macq/generate/pddl/generator.py @@ -279,7 +279,10 @@ def change_init( init = create(self.lang) for f in init_fluents: # convert fluents to tarski Atoms - atom = Atom(self.lang.get_predicate(f.name), [self.lang.get(o.name) for o in f.objects]) + atom = Atom( + self.lang.get_predicate(f.name), + [self.lang.get(o.name) for o in f.objects], + ) init.add(atom.predicate, *atom.subterms) self.problem.init = init @@ -338,7 +341,7 @@ def change_goal( self.pddl_dom = new_domain self.pddl_prob = new_prob - def generate_plan(self, from_ipc_file:bool=False, filename:str=None): + def generate_plan(self, from_ipc_file: bool = False, filename: str = None): """Generates a plan. If reading from an IPC file, the `Plan` is read directly. Otherwise, if the initial state or goal was changed, these changes are taken into account through the updated PDDL files. If no changes were made, the default nitial state/goal in the initial problem file is used. @@ -369,8 +372,8 @@ def generate_plan(self, from_ipc_file:bool=False, filename:str=None): plan = [act["name"] for act in resp["result"]["plan"]] else: f = open(filename, "r") - plan = list(filter(lambda x: ';' not in x, f.read().splitlines())) - + plan = list(filter(lambda x: ";" not in x, f.read().splitlines())) + # convert to a list of tarski PlainOperators (actions) return Plan([self.op_dict[p] for p in plan if p in self.op_dict]) @@ -400,4 +403,4 @@ def generate_single_trace_from_plan(self, plan: Plan): state = progress(state, act) else: trace.append(Step(macq_state, None, i + 1)) - return trace \ No newline at end of file + return trace diff --git a/macq/generate/pddl/random_goal_sampling.py b/macq/generate/pddl/random_goal_sampling.py index 054dde66..20e7b641 100644 --- a/macq/generate/pddl/random_goal_sampling.py +++ b/macq/generate/pddl/random_goal_sampling.py @@ -8,14 +8,13 @@ from ...utils.timer import basic_timer - MAX_GOAL_SEARCH_TIME = 30.0 class RandomGoalSampling(VanillaSampling): """Random Goal State Trace Sampler - inherits the VanillaSampling class and its attributes. - A state trace generator that generates traces by randomly generating some candidate states/goals k steps deep, + A state trace generator that generates traces by randomly generating some candidate states/goals k steps deep, then running a planner on a random subset of the fluents to get plans. The longest plans (those closest to k, thus representing goal states that are somewhat complex and take longer to reach) are taken and used to generate traces. @@ -30,8 +29,9 @@ class RandomGoalSampling(VanillaSampling): The percentage of fluents to extract to use as a goal state from the generated states. goals_inits_plans (List[Dict]): A list of dictionaries, where each dictionary stores the generated goal state as the key and the initial state and plan used to - reach the goal as values. + reach the goal as values. """ + def __init__( self, steps_deep: int, @@ -66,16 +66,18 @@ def __init__( """ if subset_size_perc < 0 or subset_size_perc > 1: raise PercentError() - self.steps_deep = steps_deep + self.steps_deep = steps_deep self.enforced_hill_climbing_sampling = enforced_hill_climbing_sampling self.subset_size_perc = subset_size_perc self.goals_inits_plans = [] - super().__init__(dom=dom, prob=prob, problem_id=problem_id, num_traces=num_traces) + super().__init__( + dom=dom, prob=prob, problem_id=problem_id, num_traces=num_traces + ) def goal_sampling(self): """Samples goals by randomly generating candidate goal states k (`steps_deep`) steps deep, then running planners on those - goal states to ensure the goals are complex enough (i.e. cannot be reached in too few steps). Candidate - goal states are generated for a set amount of time indicated by MAX_GOAL_SEARCH_TIME, and the goals with the + goal states to ensure the goals are complex enough (i.e. cannot be reached in too few steps). Candidate + goal states are generated for a set amount of time indicated by MAX_GOAL_SEARCH_TIME, and the goals with the longest plans (the most complex goals) are selected. Returns: An OrderedDict holding the longest goal states along with the initial state and plans used to reach them. @@ -83,8 +85,10 @@ def goal_sampling(self): goal_states = {} self.generate_goals(goal_states=goal_states) # sort the results by plan length and get the k largest ones - filtered_goals = OrderedDict(sorted(goal_states.items(), key=lambda x : len(x[1]["plan"].actions))) - to_del = list(filtered_goals.keys())[:len(filtered_goals) - self.num_traces] + filtered_goals = OrderedDict( + sorted(goal_states.items(), key=lambda x: len(x[1]["plan"].actions)) + ) + to_del = list(filtered_goals.keys())[: len(filtered_goals) - self.num_traces] for d in to_del: del filtered_goals[d] return filtered_goals @@ -119,12 +123,8 @@ def generate_goals(self, goal_states: Dict): self.change_goal(goal_fluents=goal_f) # ensure that the goal doesn't hold in the initial state; restart if it does - init_state = { - str(a) for a in self.problem.init.as_atoms() - } - goal = { - str(a) for a in self.problem.goal.subformulas - } + init_state = {str(a) for a in self.problem.init.as_atoms()} + goal = {str(a) for a in self.problem.goal.subformulas} if goal.issubset(init_state): continue @@ -141,7 +141,10 @@ def generate_goals(self, goal_states: Dict): for f in goal_f: state_dict[f] = True # map each goal to the initial state and plan used to achieve it - goal_states[State(state_dict)] = {"plan": test_plan, "initial state": self.problem.init} + goal_states[State(state_dict)] = { + "plan": test_plan, + "initial state": self.problem.init, + } # optionally change the initial state of the sampler for the next iteration to the goal state just generated (ensures more diversity in goals/plans) # use the full state the goal was extracted from as the initial state to prevent planning errors from incomplete initial states @@ -152,8 +155,8 @@ def generate_goals(self, goal_states: Dict): if len(test_plan.actions) >= self.steps_deep: k_length_plans += 1 if k_length_plans >= self.num_traces: - break - + break + def generate_traces(self): """Generates traces based on the sampled goals. Traces are generated using the initial state and plan used to achieve the goal. @@ -169,7 +172,5 @@ def generate_traces(self): if self.enforced_hill_climbing_sampling: self.problem.init = goal["initial state"] # generate a plan based on the new goal/initial state, then generate a trace based on that plan - traces.append( - self.generate_single_trace_from_plan(goal["plan"]) - ) - return traces \ No newline at end of file + traces.append(self.generate_single_trace_from_plan(goal["plan"])) + return traces diff --git a/macq/generate/pddl/vanilla_sampling.py b/macq/generate/pddl/vanilla_sampling.py index 4d9f6449..f805dec5 100644 --- a/macq/generate/pddl/vanilla_sampling.py +++ b/macq/generate/pddl/vanilla_sampling.py @@ -1,7 +1,13 @@ from tarski.search.operations import progress import random from . import Generator -from ...utils import set_timer_throw_exc, TraceSearchTimeOut, basic_timer, set_num_traces, set_plan_length +from ...utils import ( + set_timer_throw_exc, + TraceSearchTimeOut, + basic_timer, + set_num_traces, + set_plan_length, +) from ...observation.partial_observation import PercentError from ...trace import ( Step, @@ -116,5 +122,3 @@ def generate_single_trace(self, plan_len: int = None): trace.append(step) valid_trace = True return trace - - diff --git a/macq/generate/plan.py b/macq/generate/plan.py index 8837c60a..12cf572e 100644 --- a/macq/generate/plan.py +++ b/macq/generate/plan.py @@ -1,6 +1,7 @@ from typing import List from tarski.fstrips.action import PlainOperator + class Plan: """A Plan. @@ -11,6 +12,7 @@ class Plan: actions (List[PlainOperator]): The list of actions that make up the plan. """ + def __init__(self, actions: List[PlainOperator]): """Creates a Plan by instantiating it with the list of actions (of tarski type `PlainOperator`). @@ -43,4 +45,4 @@ def __str__(self): def __eq__(self, other): if isinstance(other, Plan): - return self.actions == other.actions \ No newline at end of file + return self.actions == other.actions diff --git a/macq/utils/__init__.py b/macq/utils/__init__.py index 4e75bb8e..8a3b6eab 100644 --- a/macq/utils/__init__.py +++ b/macq/utils/__init__.py @@ -4,4 +4,14 @@ from .trace_errors import InvalidPlanLength, InvalidNumberOfTraces from .trace_utils import set_num_traces, set_plan_length -__all__ = ["set_timer_throw_exc", "basic_timer", "TraceSearchTimeOut", "ComplexEncoder", "PercentError", "set_num_traces", "set_plan_length", "InvalidPlanLength", "InvalidNumberOfTraces"] +__all__ = [ + "set_timer_throw_exc", + "basic_timer", + "TraceSearchTimeOut", + "ComplexEncoder", + "PercentError", + "set_num_traces", + "set_plan_length", + "InvalidPlanLength", + "InvalidNumberOfTraces", +] diff --git a/macq/utils/common_errors.py b/macq/utils/common_errors.py index 681bcc9d..8c8b046f 100644 --- a/macq/utils/common_errors.py +++ b/macq/utils/common_errors.py @@ -5,4 +5,4 @@ def __init__( self, message="The percentage supplied is invalid.", ): - super().__init__(message) \ No newline at end of file + super().__init__(message) diff --git a/tests/extract/test_observer.py b/tests/extract/test_observer.py index e00c4aed..2d550a1d 100644 --- a/tests/extract/test_observer.py +++ b/tests/extract/test_observer.py @@ -19,12 +19,18 @@ def test_observer(): if __name__ == "__main__": # exit out to the base macq folder so we can get to /tests base = Path(__file__).parent.parent - model_blocks_dom = str((base / "generated_testing_files/model_blocks_domain.pddl").resolve()) - model_blocks_prob = str((base / "generated_testing_files/model_blocks_problem.pddl").resolve()) + model_blocks_dom = str( + (base / "generated_testing_files/model_blocks_domain.pddl").resolve() + ) + model_blocks_prob = str( + (base / "generated_testing_files/model_blocks_problem.pddl").resolve() + ) traces = blocks_world(5) observations = traces.tokenize(IdentityObservation) traces.print() model = Extract(observations, modes.OBSERVER) print(model.details()) - model.to_pddl("model_blocks_dom", "model_blocks_prob", model_blocks_dom, model_blocks_prob) + model.to_pddl( + "model_blocks_dom", "model_blocks_prob", model_blocks_dom, model_blocks_prob + ) diff --git a/tests/extract/test_slaf.py b/tests/extract/test_slaf.py index 6ce11f2d..247b3fc6 100644 --- a/tests/extract/test_slaf.py +++ b/tests/extract/test_slaf.py @@ -23,8 +23,12 @@ def test_slaf(): if __name__ == "__main__": # exit out to the base macq folder so we can get to /tests base = Path(__file__).parent.parent - model_blocks_dom = str((base / "generated_testing_files/model_blocks_domain.pddl").resolve()) - model_blocks_prob = str((base / "generated_testing_files/model_blocks_problem.pddl").resolve()) + model_blocks_dom = str( + (base / "generated_testing_files/model_blocks_domain.pddl").resolve() + ) + model_blocks_prob = str( + (base / "generated_testing_files/model_blocks_problem.pddl").resolve() + ) traces = generate_blocks_traces(plan_len=2, num_traces=1) observations = traces.tokenize( @@ -36,4 +40,6 @@ def test_slaf(): model = Extract(observations, modes.SLAF, debug_mode=True) print(model.details()) - model.to_pddl("model_blocks_dom", "model_blocks_prob", model_blocks_dom, model_blocks_prob) + model.to_pddl( + "model_blocks_dom", "model_blocks_prob", model_blocks_dom, model_blocks_prob + ) diff --git a/tests/generate/pddl/test_plan.py b/tests/generate/pddl/test_plan.py index 0efe17c6..f09e800d 100644 --- a/tests/generate/pddl/test_plan.py +++ b/tests/generate/pddl/test_plan.py @@ -20,4 +20,4 @@ plan = vanilla.generate_plan(from_ipc_file=True, filename=path) tracelist = TraceList() tracelist.append(vanilla.generate_single_trace_from_plan(plan)) - tracelist.print(wrap="y") \ No newline at end of file + tracelist.print(wrap="y") diff --git a/tests/generate/pddl/test_random_goal_sampling.py b/tests/generate/pddl/test_random_goal_sampling.py index baf81498..2f9de1a2 100644 --- a/tests/generate/pddl/test_random_goal_sampling.py +++ b/tests/generate/pddl/test_random_goal_sampling.py @@ -14,8 +14,7 @@ num_traces=3, steps_deep=10, subset_size_perc=0.1, - enforced_hill_climbing_sampling=False + enforced_hill_climbing_sampling=False, ) traces = random_sampler.traces traces.print(wrap="y") - diff --git a/tests/generate/pddl/test_trace_from_goal.py b/tests/generate/pddl/test_trace_from_goal.py index 8920f970..23014b84 100644 --- a/tests/generate/pddl/test_trace_from_goal.py +++ b/tests/generate/pddl/test_trace_from_goal.py @@ -32,8 +32,12 @@ def test_invalid_goal_change(): dom = str((base / "pddl_testing_files/blocks_domain.pddl").resolve()) prob = str((base / "pddl_testing_files/blocks_problem.pddl").resolve()) - new_blocks_dom = str((base / "generated_testing_files/new_blocks_dom.pddl").resolve()) - new_blocks_prob = str((base / "generated_testing_files/new_blocks_prob.pddl").resolve()) + new_blocks_dom = str( + (base / "generated_testing_files/new_blocks_dom.pddl").resolve() + ) + new_blocks_prob = str( + (base / "generated_testing_files/new_blocks_prob.pddl").resolve() + ) new_game_dom = str((base / "generated_testing_files/new_game_dom.pddl").resolve()) new_game_prob = str((base / "generated_testing_files/new_game_prob.pddl").resolve()) diff --git a/tests/generate/pddl/test_vanilla_sampling.py b/tests/generate/pddl/test_vanilla_sampling.py index a87a0100..c33f00d6 100644 --- a/tests/generate/pddl/test_vanilla_sampling.py +++ b/tests/generate/pddl/test_vanilla_sampling.py @@ -39,8 +39,12 @@ def test_invalid_vanilla_sampling(): prob = str((base / "pddl_testing_files/blocks_problem.pddl").resolve()) vanilla = VanillaSampling(dom=dom, prob=prob, plan_len=7, num_traces=10) - new_blocks_dom = str((base / "generated_testing_files/new_blocks_dom.pddl").resolve()) - new_blocks_prob = str((base / "generated_testing_files/new_blocks_prob.pddl").resolve()) + new_blocks_dom = str( + (base / "generated_testing_files/new_blocks_dom.pddl").resolve() + ) + new_blocks_prob = str( + (base / "generated_testing_files/new_blocks_prob.pddl").resolve() + ) new_game_dom = str((base / "generated_testing_files/new_game_dom.pddl").resolve()) new_game_prob = str((base / "generated_testing_files/new_game_prob.pddl").resolve()) @@ -84,4 +88,3 @@ def test_invalid_vanilla_sampling(): tracelist = TraceList() tracelist.append(trace) tracelist.print(wrap="y") - From af24881894630b12815833fafee713b43c235941 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 4 Aug 2021 11:14:16 -0400 Subject: [PATCH 069/181] Add debugging functionality for step 1 --- macq/extract/arms.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 33b0f5d1..faa0e1d5 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -123,7 +123,11 @@ def _arms( """The main driver for the ARMS algorithm.""" learned_actions = set() - connected_actions, action_map = ARMS._step1(obs_lists, debug) + debug1 = ARMS.debug_menu("Debug step 1?") if debug else False + connected_actions, action_map = ARMS._step1(obs_lists, debug1) + + if debug1: + input("Press enter to continue...") early_actions = [0] * len(obs_lists) @@ -184,7 +188,11 @@ def _arms( early_actions[i] += 1 if debug: - ARMS.debug(action=action) + print() + print(action.details()) + print("precond:", action.precond) + print("add:", action.add) + print("delete:", action.delete) if ( max([len(action.precond), len(action.add), len(action.delete)]) @@ -196,8 +204,8 @@ def _arms( ) setA.add(action) + # Update Λ by Λ − A for action in setA: - # advance early states action_keys = action_map_rev[action] for obs_action in action_keys: del action_map[obs_action] @@ -208,6 +216,8 @@ def _arms( ] for a1 in action_keys: del connected_actions[a1][action] + + # Update Θ by adding A learned_actions.add(action) # TODO @@ -243,6 +253,10 @@ def _step1( intersection = a1.obj_params.intersection(a2.obj_params) if intersection: connected_actions[a1][a2] = intersection + if debug: + print( + f"{a1.details()} is connected to {a2.details()} by {intersection}" + ) return connected_actions, action_map @@ -666,6 +680,10 @@ def _step5( relation_map = {p.var(): p for p in relations} for constraint, val in list(model.items())[:25]: + # TODO: Can you get the weight of each constraint in the model? + # if so, select only the n highest weighted constraints + # or, total the weight per action and select all constraints for + # the n highest actions. constraint = str(constraint).split("_") fluent = relation_map[constraint[0]] relation = constraint[0] @@ -702,10 +720,8 @@ def _step5( a2 = constraint[3] @staticmethod - def debug(action=None): - if action: - print() - print(action.details()) - print("precond:", action.precond) - print("add:", action.add) - print("delete:", action.delete) + def debug_menu(prompt: str): + choice = input(prompt + " (y/n): ").lower() + while choice not in ["y", "n"]: + choice = input(prompt + " (y/n): ").lower() + return choice == "y" From 34a63b3967727b97aae06a8ebf8ff29d1a43be46 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 4 Aug 2021 11:52:08 -0400 Subject: [PATCH 070/181] Add debug menus for each step in debug mode --- macq/extract/arms.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index faa0e1d5..1c7c77b1 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -121,24 +121,33 @@ def _arms( debug: bool, ) -> Set[LearnedAction]: """The main driver for the ARMS algorithm.""" - learned_actions = set() + learned_actions = set() # The set of learned action models Θ + # pointers to the earliest unlearned action for each observation list + early_actions = [0] * len(obs_lists) debug1 = ARMS.debug_menu("Debug step 1?") if debug else False connected_actions, action_map = ARMS._step1(obs_lists, debug1) - if debug1: input("Press enter to continue...") - early_actions = [0] * len(obs_lists) - action_map_rev: Dict[LearnedAction, List[Action]] = defaultdict(list) for obs_action, learned_action in action_map.items(): action_map_rev[learned_action].append(obs_action) + count = 1 + debug2 = ARMS.debug_menu("Debug step 2?") if debug else False + debug3 = ARMS.debug_menu("Debug step 3?") if debug else False + debug4 = ARMS.debug_menu("Debug step 4?") if debug else False + debug5 = ARMS.debug_menu("Debug step 5?") if debug else False while action_map_rev: + print("Iteration", count) + count += 1 + constraints, relation_map = ARMS._step2( - obs_lists, connected_actions, action_map, fluents, min_support, debug + obs_lists, connected_actions, action_map, fluents, min_support, debug2 ) + if debug2: + input("Press enter to continue...") relation_map_rev: Dict[Relation, List[Fluent]] = defaultdict(list) for fluent, relation in relation_map.items(): @@ -151,15 +160,21 @@ def _arms( threshold, info3_default, plan_default, - debug, + debug3, ) + if debug3: + input("Press enter to continue...") - model = ARMS._step4(max_sat, decode, debug) + model = ARMS._step4(max_sat, decode, debug4) + if debug4: + input("Press enter to continue...") # Mutates the LearnedAction (keys) of action_map_rev ARMS._step5( - model, list(action_map_rev.keys()), list(relation_map.values()), debug + model, list(action_map_rev.keys()), list(relation_map.values()), debug5 ) + if debug5: + input("Press enter to continue...") # Step 5 updates setA = set() From dae2d1171c36b223a1a41b143f919c7ec1d0efb7 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 6 Aug 2021 14:28:12 -0400 Subject: [PATCH 071/181] Parse plan constraints after action and info --- macq/extract/arms.py | 56 ++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 1c7c77b1..7ab9ee7b 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -135,14 +135,11 @@ def _arms( action_map_rev[learned_action].append(obs_action) count = 1 - debug2 = ARMS.debug_menu("Debug step 2?") if debug else False - debug3 = ARMS.debug_menu("Debug step 3?") if debug else False - debug4 = ARMS.debug_menu("Debug step 4?") if debug else False - debug5 = ARMS.debug_menu("Debug step 5?") if debug else False while action_map_rev: print("Iteration", count) count += 1 + debug2 = ARMS.debug_menu("Debug step 2?") if debug else False constraints, relation_map = ARMS._step2( obs_lists, connected_actions, action_map, fluents, min_support, debug2 ) @@ -153,6 +150,7 @@ def _arms( for fluent, relation in relation_map.items(): relation_map_rev[relation].append(fluent) + debug3 = ARMS.debug_menu("Debug step 3?") if debug else False max_sat, decode = ARMS._step3( constraints, action_weight, @@ -165,10 +163,12 @@ def _arms( if debug3: input("Press enter to continue...") + debug4 = ARMS.debug_menu("Debug step 4?") if debug else False model = ARMS._step4(max_sat, decode, debug4) if debug4: input("Press enter to continue...") + debug5 = ARMS.debug_menu("Debug step 5?") if debug else False # Mutates the LearnedAction (keys) of action_map_rev ARMS._step5( model, list(action_map_rev.keys()), list(relation_map.values()), debug5 @@ -235,9 +235,7 @@ def _arms( # Update Θ by adding A learned_actions.add(action) - # TODO - # return set(learned_actions.values()) - return set() + return learned_actions @staticmethod def _step1( @@ -301,20 +299,25 @@ def _step2( ) ) + debuga = ARMS.debug_menu("Debug action constraints?") if debug else False + action_constraints = ARMS._step2A( - connected_actions, set(relations.values()), debug + connected_actions, set(relations.values()), debuga ) + debugi = ARMS.debug_menu("Debug info constraints?") if debug else False info_constraints, info_support_counts = ARMS._step2I( - obs_lists, relations, action_map, debug + obs_lists, relations, action_map, debugi ) + debugp = ARMS.debug_menu("Debug plan constraints?") if debug else False plan_constraints = ARMS._step2P( obs_lists, connected_actions, action_map, set(relations.values()), min_support, + debugp, ) return ( @@ -525,8 +528,8 @@ def _step2P( action_map: Dict[Action, LearnedAction], relations: Set[Relation], min_support: int, + debug: bool, ) -> Dict[Or[Var], int]: - # ) -> Dict[And[Or[Var]], int]: frequent_pairs = ARMS._apriori( [ [ @@ -673,7 +676,7 @@ def _step4( ) -> Dict[Hashable, bool]: solver = RC2(max_sat) solver.compute() - encoded_model = solver.model + encoded_model = solver.compute() if not isinstance(encoded_model, list): raise InvalidMaxSATModel(encoded_model) @@ -693,8 +696,10 @@ def _step5( ): action_map = {a.details(): a for a in actions} relation_map = {p.var(): p for p in relations} + negative_constraints = defaultdict(set) + plan_constraints = [] - for constraint, val in list(model.items())[:25]: + for constraint, val in list(model.items()): # TODO: Can you get the weight of each constraint in the model? # if so, select only the n highest weighted constraints # or, total the weight per action and select all constraints for @@ -714,7 +719,6 @@ def _step5( if effect == "add" else action.update_delete ) - # action_update({str(fluent)}) action_update({relation}) else: action_effect = ( @@ -725,14 +729,26 @@ def _step5( else action.delete ) if fluent in action_effect: - # TODO: determine if this is an error, or if it just - # means the effect should be removed (due to more info - # from later iterations) raise ConstraintContradiction(fluent, effect, action) - else: # plan constraint - # doesn't directly affect actions - a1 = constraint[2] - a2 = constraint[3] + negative_constraints[(relation, action)].add(effect) + + else: # store plan constraint + ai = action_map[constraint[2]] + aj = action_map[constraint[3]] + plan_constraints.append([relation, ai, aj]) + + for p, ai, aj in plan_constraints: + # one of the following must be true + if not ( + (p in ai.precond and p in aj.precond and p not in ai.delete) + or (p in ai.add and p in aj.precond) + or (p in ai.delete and p in aj.add) + ): + # if not, filter down which should be true by checking for negative constraints + if (p, ai) in negative_constraints: + pass + elif (p, aj) in negative_constraints: + pass @staticmethod def debug_menu(prompt: str): From c57092deaead519b20dc0ddad70e67aa89f8ae62 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 6 Aug 2021 15:29:29 -0400 Subject: [PATCH 072/181] Mutate action effects based on plan constraints --- macq/extract/arms.py | 73 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 7ab9ee7b..1ac434c7 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -697,7 +697,7 @@ def _step5( action_map = {a.details(): a for a in actions} relation_map = {p.var(): p for p in relations} negative_constraints = defaultdict(set) - plan_constraints = [] + plan_constraints: List[Tuple[str, LearnedAction, LearnedAction]] = [] for constraint, val in list(model.items()): # TODO: Can you get the weight of each constraint in the model? @@ -735,20 +735,73 @@ def _step5( else: # store plan constraint ai = action_map[constraint[2]] aj = action_map[constraint[3]] - plan_constraints.append([relation, ai, aj]) + plan_constraints.append((relation, ai, aj)) for p, ai, aj in plan_constraints: # one of the following must be true if not ( - (p in ai.precond and p in aj.precond and p not in ai.delete) - or (p in ai.add and p in aj.precond) - or (p in ai.delete and p in aj.add) + (p in ai.precond.intersection(aj.precond) and p not in ai.delete) # P3 + or (p in ai.add.intersection(aj.precond)) # P4 + or (p in ai.delete.intersection(aj.add)) # P5 ): - # if not, filter down which should be true by checking for negative constraints - if (p, ai) in negative_constraints: - pass - elif (p, aj) in negative_constraints: - pass + # check if either P3 or P4 are partially fulfilled and can be satisfied + if p in ai.precond.union(aj.precond): + if p in aj.precond: + # if P3 isn't contradicted, add p to ai.precond + if p not in ai.delete and not ( + (p, ai) in negative_constraints + and "pre" in negative_constraints[(p, ai)] + ): + ai.update_precond({p}) + + # if P4 isn't contradicted, add p to ai.add + if not ( + (p, ai) in negative_constraints + and "add" in negative_constraints[(p, ai)] + ): + ai.update_add({p}) + + # p in ai.precond and P3 not contradicted, add p to aj.precond + elif p not in ai.delete and not ( + (p, aj) in negative_constraints + and "pre" in negative_constraints[(p, aj)] + ): + aj.update_precond({p}) + + # check if either P3 or P4 can be satisfied + elif not ( + (p, aj) in negative_constraints + and "pre" in negative_constraints[(p, aj)] + ): + # if P3 isn't contradicted, add p to both ai and aj preconds + if p not in ai.delete and not ( + (p, ai) in negative_constraints + and "pre" in negative_constraints[(p, ai)] + ): + ai.update_precond({p}) + aj.update_precond({p}) + + # if P4 isn't contradicted, add p to ai.add and aj.precond + if not ( + (p, ai) in negative_constraints + and "add" in negative_constraints[(p, ai)] + ): + ai.update_add({p}) + aj.update_precond({p}) + + # check if P5 can be satisfied + # if P5 isn't contradicted, add p wherever it is missing + if not ( + (p, ai) in negative_constraints + and "del" in negative_constraints[(p, ai)] + ) and not ( + (p, aj) in negative_constraints + and "add" in negative_constraints[(p, aj)] + ): + if p not in ai.delete: + ai.update_delete({p}) + if p not in aj.add: + aj.update_add({p}) @staticmethod def debug_menu(prompt: str): From aeba37cc8679dadda2222a5028bf073e2c301025 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 6 Aug 2021 17:04:58 -0400 Subject: [PATCH 073/181] Fix constraint parsing --- macq/extract/arms.py | 81 ++++++++++++++------------------- macq/extract/learned_action.py | 5 ++ macq/generate/pddl/generator.py | 9 ++-- 3 files changed, 42 insertions(+), 53 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 1ac434c7..272ce64a 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -105,6 +105,7 @@ def __new__( plan_default, debug, ) + return Model(fluents, actions) @staticmethod @@ -171,7 +172,11 @@ def _arms( debug5 = ARMS.debug_menu("Debug step 5?") if debug else False # Mutates the LearnedAction (keys) of action_map_rev ARMS._step5( - model, list(action_map_rev.keys()), list(relation_map.values()), debug5 + model, + list(action_map_rev.keys()), + list(relation_map.values()), + upper_bound, + debug5, ) if debug5: input("Press enter to continue...") @@ -367,14 +372,22 @@ def implication(a: Var, b: Var): # for parsing constraints later. constraints.append( implication( - Var(f"{relation.var()}_in_add_{action.details()}"), - Var(f"{relation.var()}_in_pre_{action.details()}").negate(), + Var( + f"{relation.var()}_BREAK_in_BREAK_add_BREAK_{action.details()}" + ), + Var( + f"{relation.var()}_BREAK_in_BREAK_pre_BREAK_{action.details()}" + ).negate(), ) ) constraints.append( implication( - Var(f"{relation.var()}_in_pre_{action.details()}"), - Var(f"{relation.var()}_in_add_{action.details()}").negate(), + Var( + f"{relation.var()}_BREAK_in_BREAK_pre_BREAK_{action.details()}" + ), + Var( + f"{relation.var()}_BREAK_in_BREAK_add_BREAK_{action.details()}" + ).negate(), ) ) @@ -382,8 +395,12 @@ def implication(a: Var, b: Var): # relation in action.del => relation in action.precond constraints.append( implication( - Var(f"{relation.var()}_in_del_{action.details()}"), - Var(f"{relation.var()}_in_pre_{action.details()}"), + Var( + f"{relation.var()}_BREAK_in_BREAK_del_BREAK_{action.details()}" + ), + Var( + f"{relation.var()}_BREAK_in_BREAK_pre_BREAK_{action.details()}" + ), ) ) @@ -430,7 +447,7 @@ def _step2I( ai = actions[obs_i.action] i1.append( Var( - f"{relations[fluent].var()}_in_add_{ai.details()}" + f"{relations[fluent].var()}_BREAK_in_BREAK_add_BREAK_{ai.details()}" ) ) @@ -440,7 +457,7 @@ def _step2I( a_n = obs_list[i - 1].action if a_n in actions and a_n is not None: i2 = Var( - f"{relations[fluent].var()}_in_del_{actions[a_n].details()}" + f"{relations[fluent].var()}_BREAK_in_BREAK_del_BREAK_{actions[a_n].details()}" ).negate() if i1: @@ -460,7 +477,7 @@ def _step2I( Or( [ Var( - f"{relations[fluent].var()}_in_pre_{actions[obs.action].details()}" + f"{relations[fluent].var()}_BREAK_in_BREAK_pre_BREAK_{actions[obs.action].details()}" ) ] ) @@ -471,7 +488,7 @@ def _step2I( Or( [ Var( - f"{relations[fluent].var()}_in_add_{actions[a_n].details()}" + f"{relations[fluent].var()}_BREAK_in_BREAK_add_BREAK_{actions[a_n].details()}" ) ] ) @@ -561,39 +578,10 @@ def _step2P( # relation_constraints: List[Or[And[Var]]] = [] relation_constraints: List[Var] = [] for relation in relevant_relations: - """ - ∃p( - (p∈ (pre_i ∩ pre_j) ∧ p∉ (del_i)) ∨ - (p∈ (add_i ∩ pre_j)) ∨ - (p∈ (del_i ∩ add_j)) - ) - where p is a relevant relation. - """ - # Phi = Or( - # [ - # And( - # [ - # Var(f"{relation.var()}_in_pre_{ai.details()}"), - # Var(f"{relation.var()}_in_pre_{aj.details()}"), - # Var(f"{relation.var()}_in_del_{ai.details()}").negate(), - # ] - # ), - # And( - # [ - # Var(f"{relation.var()}_in_add_{ai.details()}"), - # Var(f"{relation.var()}_in_pre_{aj.details()}"), - # ] - # ), - # And( - # [ - # Var(f"{relation.var()}_in_del_{ai.details()}"), - # Var(f"{relation.var()}_in_add_{aj.details()}"), - # ] - # ), - # ] - # ) relation_constraints.append( - Var(f"{relation.var()}_relevant_{ai.details()}_{aj.details()}") + Var( + f"{relation.var()}_BREAK_relevant_BREAK_{ai.details()}_BREAK_{aj.details()}" + ) ) constraints[Or(relation_constraints)] = frequent_pairs[(ai, aj)] @@ -692,6 +680,7 @@ def _step5( model: Dict[Hashable, bool], actions: List[LearnedAction], relations: List[Relation], + upper_bound: int, debug: bool, ): action_map = {a.details(): a for a in actions} @@ -700,11 +689,7 @@ def _step5( plan_constraints: List[Tuple[str, LearnedAction, LearnedAction]] = [] for constraint, val in list(model.items()): - # TODO: Can you get the weight of each constraint in the model? - # if so, select only the n highest weighted constraints - # or, total the weight per action and select all constraints for - # the n highest actions. - constraint = str(constraint).split("_") + constraint = str(constraint).split("_BREAK_") fluent = relation_map[constraint[0]] relation = constraint[0] ctype = constraint[1] # constraint type diff --git a/macq/extract/learned_action.py b/macq/extract/learned_action.py index b1449bbd..9d6348d1 100644 --- a/macq/extract/learned_action.py +++ b/macq/extract/learned_action.py @@ -58,6 +58,11 @@ def update_delete(self, fluents: Set[str]): """ self.delete.update(fluents) + def clear(self): + self.precond = set() + self.add = set() + self.delete = set() + def compare(self, orig_action: LearnedAction): """Compares the learned action to an original, ground truth action.""" precond_diff = orig_action.precond.difference(self.precond) diff --git a/macq/generate/pddl/generator.py b/macq/generate/pddl/generator.py index 20f6f359..f8a01578 100644 --- a/macq/generate/pddl/generator.py +++ b/macq/generate/pddl/generator.py @@ -26,10 +26,9 @@ class InvalidGoalFluent(Exception): Raised when the user attempts to supply a new goal with invalid fluent(s). """ - def __init__( - self, - message="The fluents provided contain one or more fluents not available in this problem.", - ): + def __init__(self, fluent, message=None): + if message is None: + message = f"{fluent} is not available in this problem." super().__init__(message) @@ -317,7 +316,7 @@ def change_goal( available_f = self.grounded_fluents for f in goal_fluents: if f not in available_f: - raise InvalidGoalFluent() + raise InvalidGoalFluent(f) # convert the given set of fluents into a formula if not goal_fluents: From 132692d315b92fcea7f77231272dcba1ec8d5141 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Sun, 8 Aug 2021 14:49:42 -0400 Subject: [PATCH 074/181] set up disorder and parallel constraints --- macq/extract/amdn.py | 111 +++++++++++++++++++++++++++++++++---------- 1 file changed, 86 insertions(+), 25 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index a9d83b2c..3f1cf604 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -1,9 +1,26 @@ +from nnf.operators import implies import macq.extract as extract +from nnf import Var, And, Or from .model import Model from ..trace import ObservationLists from ..observation import NoisyPartialDisorderedParallelObservation +def __set_precond(r, act): + return Var(r + " is a precondition of " + act.name) + +def __set_del(r, act): + return Var(r + " is deleted by " + act.name) + +def __set_add(r, act): + return Var(r + " is added by " + act.name) + +# for easier reference +pre = __set_precond +add = __set_add +delete = __set_del + class AMDN: + def __new__(cls, obs_lists: ObservationLists): """Creates a new Model object. @@ -17,14 +34,19 @@ def __new__(cls, obs_lists: ObservationLists): if obs_lists.type is not NoisyPartialDisorderedParallelObservation: raise extract.IncompatibleObservationToken(obs_lists.type, AMDN) + + # TODO: # iterate through all tokens and create two base sets for all actions and propositions; store as attributes # iterate through all pairs of parallel action sets and create a dictionary of the probability of ax and ay being disordered - # (this will make it easy and efficient to refer to later, and prevents unnecessary recalculations). store as attribute # also create a list of all tuples, store as attribute + # get user values (TODO: ask: wmax is a user value...?) #return Model(fluents, actions) + + def __calculate_probability(self, action1, action2, lambda1): # TODO: @@ -39,40 +61,79 @@ def __calculate_all_probabilities(self): pass def __build_disorder_constraints(self): - # TODO: + + # iterate through all pairs of parallel action sets - # for each pair, iterate through all possible action combinations - # calculate the probability of the actions being disordered (p) - # for each action combination, iterate through all possible propositions - # for each action x action x proposition combination, enforce the following constraint if the actions are ordered: - # enforce all [constraint 1] with weight (1 - p) x wmax - - # likewise, enforce the following constraint if the actions are disordered: - # enforce all [constraint 2] with weight p x wmax - pass + for i in range(len(self.par_act_sets) - 1): + # for each pair, iterate through all possible action combinations + for act_x in self.par_act_sets[i]: + for act_y in self.par_act_sets[i + 1]: + # TODO: calculate the probability of the actions being disordered (p) + # for each action combination, iterate through all possible propositions + # TODO: calculate weights + for r in self.propositions: + # enforce the following constraint if the actions are ordered with weight (1 - p) x wmax: + constraint1 = Or([ + And([pre(r, act_x), ~delete(r, act_x), delete(r, act_y)]), + And([add(r, act_x), pre(r, act_y)]), + And([add(r, act_x), delete(r, act_y)]), + And([delete(r, act_x), add(r, act_y)]) + ]) + # likewise, enforce the following constraint if the actions are disordered with weight p x wmax: + constraint2 = Or([ + And([pre(r, act_y), ~delete(r, act_y), delete(r, act_x)]), + And([add(r, act_y), pre(r, act_x)]), + And([add(r, act_y), delete(r, act_x)]), + And([delete(r, act_y), add(r, act_x)]) + ]) def __build_hard_parallel_constraints(self): - # TODO: + # for easier reference + pre = self.__set_precond + add = self.__set_add + delete = self.__set_del + # iterate through the list of tuples - # for each action x proposition pair, enforce the two hard constraints with weight wmax - pass + for tup in self.act_prop_cross_prod: + act = tup[0] + r = tup[1] + # for each action x proposition pair, enforce the two hard constraints with weight wmax + constraint1 = implies(add(r, act), ~pre(r, act)) + constraint2 = implies(delete(r, act), pre(r, act)) + # TODO: use weight wmax def __build_soft_parallel_constraints(self): - # TODO: + constraints_4 = set() + constraints_5 = set() # iterate through all parallel action sets - # within each parallel action set, iterate through the same action set again to compare - # each action to every other action in the set; assuming none are disordered - # enforce all [constraint 4] with weight (1 - p) x wmax - - # then, iterate through all pairs of action sets - # assuming the actions are disordered, check ay against EACH action in ax, for each pair - # enforce all [constraint 5] with weight p x wmax - pass + for i in range(len(self.par_act_sets)): + # within each parallel action set, iterate through the same action set again to compare + # each action to every other action in the set; setting constraints assuming actions are not disordered + for act_x in self.par_act_sets[i]: + for act_x_prime in self.par_act_sets[i]: + if act_x != act_x_prime: + # iterate through all propositions + for r in self.propositions: + # equivalent: if r is in the add or delete list of an action in the set, that implies it + # can't be in the add or delete list of any other action in the set + constraints_4.add(implies(Or([add(r, act_x)], delete(r, act_x)), ~Or([add(r, act_x_prime)], delete(r, act_x_prime)))) + # TODO: enforce all [constraint 4] with weight (1 - p) x wmax + + # then, iterate through all pairs of parallel action sets + for i in range(len(self.par_act_sets) - 1): + # for each pair, compare every action in act_y to every action in act_x_prime; setting constraints + # assuming actions are disordered + for act_y in self.par_act_sets[i + 1]: + for act_x_prime in self.par_act_sets[i]: + if act_y != act_x_prime: + # iterate through all propositions and similarly set the constraint + for r in self.propositions: + constraints_5.add(implies(Or([add(r, act_y)], delete(r, act_y)), ~Or([add(r, act_x_prime)], delete(r, act_x_prime)))) + # TODO: enforce all [constraint 5] with weight p x wmax def __build_parallel_constraints(self): - # TODO: - # call the above two functions - pass + self.__build_hard_parallel_constraints() + self.__build_soft_parallel_constraints() def __build_noise_constraints(self): # TODO: From c69428f90a8c6b70aad883f27cea16f14fc55eb9 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 9 Aug 2021 16:35:46 -0400 Subject: [PATCH 075/181] Update docs --- macq/extract/arms.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 272ce64a..0f0103f4 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -74,10 +74,12 @@ def __new__( The minimum support count for an action pair to be considered frequent. action_weight (int): The constant weight W_A(a) to assign to each action constraint. + Should be set higher than the weight of information constraints. info_weight (int): The constant weight W_I(r) to assign to each information constraint. + Determined empirically, generally the highest in all constraints' weights. threshold (float): - (0-1) The probability threshold θ to determine if an I3/plan constraint + (0-1). The probability threshold θ to determine if an I3/plan constraint is weighted by its probability or set to a default value. info3_default (int): The default weight for I3 constraints with probability below the threshold. @@ -137,8 +139,9 @@ def _arms( count = 1 while action_map_rev: - print("Iteration", count) - count += 1 + if debug: + print("Iteration", count) + count += 1 debug2 = ARMS.debug_menu("Debug step 2?") if debug else False constraints, relation_map = ARMS._step2( @@ -665,7 +668,9 @@ def _step4( solver = RC2(max_sat) solver.compute() encoded_model = solver.compute() + if not isinstance(encoded_model, list): + # should never be reached raise InvalidMaxSATModel(encoded_model) # decode the model (back to nnf vars) @@ -688,7 +693,11 @@ def _step5( negative_constraints = defaultdict(set) plan_constraints: List[Tuple[str, LearnedAction, LearnedAction]] = [] - for constraint, val in list(model.items()): + # NOTE: only taking the top n (optimal number varies, determine + # empirically) constraints usually results in more accurate action + # models, however this is not a part of the paper and therefore not + # implemented. + for constraint, val in model.items(): constraint = str(constraint).split("_BREAK_") fluent = relation_map[constraint[0]] relation = constraint[0] From f84fc9c3b4b6bd90e3ef2f7f6eeed2619d3b9b32 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 9 Aug 2021 16:44:00 -0400 Subject: [PATCH 076/181] Add debugging to step 5 --- macq/extract/arms.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 0f0103f4..f4d30e0d 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -699,12 +699,15 @@ def _step5( # implemented. for constraint, val in model.items(): constraint = str(constraint).split("_BREAK_") - fluent = relation_map[constraint[0]] relation = constraint[0] ctype = constraint[1] # constraint type if ctype == "in": effect = constraint[2] action = action_map[constraint[3]] + if debug: + print( + f"Learned constraint: {relation} in {effect}_{action.details()}" + ) if val: action_update = ( action.update_precond @@ -722,14 +725,16 @@ def _step5( if effect == "add" else action.delete ) - if fluent in action_effect: - raise ConstraintContradiction(fluent, effect, action) + if relation in action_effect: + raise ConstraintContradiction(relation, effect, action) negative_constraints[(relation, action)].add(effect) else: # store plan constraint ai = action_map[constraint[2]] aj = action_map[constraint[3]] plan_constraints.append((relation, ai, aj)) + if debug: + print(f"{relation} possibly explains action pair ({ai}, {aj})") for p, ai, aj in plan_constraints: # one of the following must be true From 54449c260298b416ad3741260222d70e6a435250 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 10 Aug 2021 13:19:33 -0400 Subject: [PATCH 077/181] Cleanup code --- macq/extract/arms.py | 46 ++++++++++++++++---------------------- macq/extract/exceptions.py | 4 ++-- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index f4d30e0d..f711f8f6 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -2,14 +2,12 @@ from dataclasses import dataclass from logging import warn from typing import Set, List, Dict, Tuple, Hashable -from nnf import NNF, Var, And, Or, false as nnffalse +from nnf import Var, And, Or, false as nnffalse from pysat.examples.rc2 import RC2 from pysat.formula import WCNF from . import LearnedAction, Model from .exceptions import ( - ConstraintContradiction, IncompatibleObservationToken, - InconsistentConstraintWeights, InvalidMaxSATModel, ) from ..observation import PartialObservation as Observation @@ -167,18 +165,13 @@ def _arms( if debug3: input("Press enter to continue...") - debug4 = ARMS.debug_menu("Debug step 4?") if debug else False - model = ARMS._step4(max_sat, decode, debug4) - if debug4: - input("Press enter to continue...") + model = ARMS._step4(max_sat, decode) debug5 = ARMS.debug_menu("Debug step 5?") if debug else False # Mutates the LearnedAction (keys) of action_map_rev ARMS._step5( model, list(action_map_rev.keys()), - list(relation_map.values()), - upper_bound, debug5, ) if debug5: @@ -371,8 +364,7 @@ def implication(a: Var, b: Var): # relation in action.add => relation not in action.precond # relation in action.precond => relation not in action.add - # underscores are used to unambiguously mark split locations - # for parsing constraints later. + # _BREAK_ marks unambiguous breakpoints for parsing later constraints.append( implication( Var( @@ -572,7 +564,7 @@ def _step2P( if aj in connected_actions and ai in connected_actions[aj]: connectors.update(connected_actions[aj][ai]) - # if the actions are not related they are not a valid pair for a plan constraint + # if the actions are not related they are not a valid pair for a plan constraint. if not connectors: continue @@ -581,11 +573,16 @@ def _step2P( # relation_constraints: List[Or[And[Var]]] = [] relation_constraints: List[Var] = [] for relation in relevant_relations: + relation_constraints.append( Var( f"{relation.var()}_BREAK_relevant_BREAK_{ai.details()}_BREAK_{aj.details()}" ) ) + if debug: + print( + f"{relation.var()} might explain action pair ({ai.details()}, {aj.details()})" + ) constraints[Or(relation_constraints)] = frequent_pairs[(ai, aj)] return constraints @@ -625,17 +622,14 @@ def _step3( if constraint not in constraints_w_weights: constraints_w_weights[constraint] = weight elif weight != constraints_w_weights[constraint]: - warn( - f"The constraint {constraint} has conflicting weights ({weight} and {constraints_w_weights[constraint]}). Choosing the smaller weight." - ) + if debug: + warn( + f"The constraint {constraint} has conflicting weights ({weight} and {constraints_w_weights[constraint]}). Choosing the smaller weight." + ) constraints_w_weights[constraint] = min( weight, constraints_w_weights[constraint] ) - # raise InconsistentConstraintWeights( - # constraint, weight, constraints_w_weights[constraint] - # ) - problem: And[Or[Var]] = And(list(constraints_w_weights.keys())) weights = list(constraints_w_weights.values()) @@ -662,11 +656,8 @@ def get_support_rate(count): return list(map(get_support_rate, support_counts)) @staticmethod - def _step4( - max_sat: WCNF, decode: Dict[int, Hashable], debug: bool - ) -> Dict[Hashable, bool]: + def _step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: solver = RC2(max_sat) - solver.compute() encoded_model = solver.compute() if not isinstance(encoded_model, list): @@ -684,12 +675,9 @@ def _step4( def _step5( model: Dict[Hashable, bool], actions: List[LearnedAction], - relations: List[Relation], - upper_bound: int, debug: bool, ): action_map = {a.details(): a for a in actions} - relation_map = {p.var(): p for p in relations} negative_constraints = defaultdict(set) plan_constraints: List[Tuple[str, LearnedAction, LearnedAction]] = [] @@ -726,7 +714,11 @@ def _step5( else action.delete ) if relation in action_effect: - raise ConstraintContradiction(relation, effect, action) + if debug: + warn( + f"Removing {relation} from {effect} of {action.details()}" + ) + action_effect.remove(relation) negative_constraints[(relation, action)].add(effect) else: # store plan constraint diff --git a/macq/extract/exceptions.py b/macq/extract/exceptions.py index c6a473aa..7407cb23 100644 --- a/macq/extract/exceptions.py +++ b/macq/extract/exceptions.py @@ -20,7 +20,7 @@ def __init__(self, model, message=None): class ConstraintContradiction(Exception): - def __init__(self, fluent, effect, action, message=None): + def __init__(self, relation, effect, action, message=None): if message is None: - message = f"Action model has contradictory constraints for {fluent.details()}'s presence in the {effect} list of {action.details()}." + message = f"Action model has contradictory constraints for {relation}'s presence in the {effect} list of {action.details()}." super().__init__(message) From 26ffd34eefe1fdc96144057cc417a0ce521b7655 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 10 Aug 2021 17:31:51 -0400 Subject: [PATCH 078/181] add amdn option --- macq/extract/amdn.py | 7 +++---- macq/extract/extract.py | 3 +++ tests/extract/test_amdn.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index f7410f09..4b5b7ead 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -20,8 +20,7 @@ def __set_add(r, act): delete = __set_del class AMDN: - - def __new__(cls, obs_lists: ObservationLists): + def __init__(self, obs_lists: ObservationLists): """Creates a new Model object. Args: @@ -34,10 +33,10 @@ def __new__(cls, obs_lists: ObservationLists): if obs_lists.type is not NoisyPartialDisorderedParallelObservation: raise extract.IncompatibleObservationToken(obs_lists.type, AMDN) - - # TODO: # iterate through all tokens and create two base sets for all actions and propositions; store as attributes + self.actions = obs_lists.actions + # iterate through all pairs of parallel action sets and create a dictionary of the probability of ax and ay being disordered - # (this will make it easy and efficient to refer to later, and prevents unnecessary recalculations). store as attribute # also create a list of all tuples, store as attribute diff --git a/macq/extract/extract.py b/macq/extract/extract.py index ea2e84a4..6e643abb 100644 --- a/macq/extract/extract.py +++ b/macq/extract/extract.py @@ -4,6 +4,7 @@ from .model import Model from .observer import Observer from .slaf import SLAF +from .amdn import AMDN @dataclass @@ -28,6 +29,7 @@ class modes(Enum): OBSERVER = auto() SLAF = auto() + AMDN = auto() class Extract: @@ -58,6 +60,7 @@ def __new__(cls, obs_lists: ObservationLists, mode: modes, **kwargs) -> Model: techniques = { modes.OBSERVER: Observer, modes.SLAF: SLAF, + modes.AMDN: AMDN } if mode == modes.SLAF: # only allow one trace diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 0c4bd045..361cf8fa 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -38,5 +38,5 @@ def test_tokenization_error(): percent_noisy=0.05, ) print() - #model = Extract(observations, modes.SLAF, debug_mode=True) + model = Extract(observations, modes.AMDN) #print(model.details()) \ No newline at end of file From 68d0465b31c9dbfc47578d5ecf9af68d888f9248 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Wed, 11 Aug 2021 11:48:01 -0400 Subject: [PATCH 079/181] full algorithm implementation first pass --- macq/extract/amdn.py | 222 +++++++++++------- macq/trace/__init__.py | 5 +- ...ered_parallel_actions_observation_lists.py | 3 + 3 files changed, 147 insertions(+), 83 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 4b5b7ead..cc4a47af 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -2,7 +2,7 @@ import macq.extract as extract from nnf import Var, And, Or from .model import Model -from ..trace import ObservationLists +from ..trace import ObservationLists, ActionPair from ..observation import NoisyPartialDisorderedParallelObservation def __set_precond(r, act): @@ -20,7 +20,8 @@ def __set_add(r, act): delete = __set_del class AMDN: - def __init__(self, obs_lists: ObservationLists): + # TODO: ask: wmax is a user value...? + def __init__(self, obs_lists: ObservationLists, wmax: float, occ_threshold: int): """Creates a new Model object. Args: @@ -33,61 +34,61 @@ def __init__(self, obs_lists: ObservationLists): if obs_lists.type is not NoisyPartialDisorderedParallelObservation: raise extract.IncompatibleObservationToken(obs_lists.type, AMDN) - # TODO: - # iterate through all tokens and create two base sets for all actions and propositions; store as attributes + # create two base sets for all actions and propositions; store as attributes self.actions = obs_lists.actions + self.propositions = {f for trace in obs_lists for step in trace for f in step.state.fluents} + # get probability dictionary + self.probabilities = obs_lists.probabilities + # get parallel action sets + self.par_act_sets = obs_lists.par_act_sets - # iterate through all pairs of parallel action sets and create a dictionary of the probability of ax and ay being disordered - - # (this will make it easy and efficient to refer to later, and prevents unnecessary recalculations). store as attribute - # also create a list of all tuples, store as attribute - # get user values (TODO: ask: wmax is a user value...?) + self.wmax = wmax + self.occ_threshold = occ_threshold #return Model(fluents, actions) + + def _build_disorder_constraints(self): - # TODO: + disorder_constraints = {} # iterate through all pairs of parallel action sets for i in range(len(self.par_act_sets) - 1): # for each pair, iterate through all possible action combinations for act_x in self.par_act_sets[i]: for act_y in self.par_act_sets[i + 1]: - # TODO: calculate the probability of the actions being disordered (p) - # for each action combination, iterate through all possible propositions - # TODO: calculate weights - for r in self.propositions: - # enforce the following constraint if the actions are ordered with weight (1 - p) x wmax: - constraint1 = Or([ - And([pre(r, act_x), ~delete(r, act_x), delete(r, act_y)]), - And([add(r, act_x), pre(r, act_y)]), - And([add(r, act_x), delete(r, act_y)]), - And([delete(r, act_x), add(r, act_y)]) - ]) - # likewise, enforce the following constraint if the actions are disordered with weight p x wmax: - constraint2 = Or([ - And([pre(r, act_y), ~delete(r, act_y), delete(r, act_x)]), - And([add(r, act_y), pre(r, act_x)]), - And([add(r, act_y), delete(r, act_x)]), - And([delete(r, act_y), add(r, act_x)]) - ]) - - def __build_hard_parallel_constraints(self): - # for easier reference - pre = self.__set_precond - add = self.__set_add - delete = self.__set_del - - # iterate through the list of tuples - for tup in self.act_prop_cross_prod: - act = tup[0] - r = tup[1] - # for each action x proposition pair, enforce the two hard constraints with weight wmax - constraint1 = implies(add(r, act), ~pre(r, act)) - constraint2 = implies(delete(r, act), pre(r, act)) - # TODO: use weight wmax - - def __build_soft_parallel_constraints(self): - constraints_4 = set() - constraints_5 = set() + if act_x != act_y: + # calculate the probability of the actions being disordered (p) + p = self.probabilities[ActionPair({act_x, act_y})] + # for each action combination, iterate through all possible propositions + for r in self.propositions: + # enforce the following constraint if the actions are ordered with weight (1 - p) x wmax: + disorder_constraints[Or([ + And([pre(r, act_x), ~delete(r, act_x), delete(r, act_y)]), + And([add(r, act_x), pre(r, act_y)]), + And([add(r, act_x), delete(r, act_y)]), + And([delete(r, act_x), add(r, act_y)]) + ])] = (1 - p) * self.wmax + # likewise, enforce the following constraint if the actions are disordered with weight p x wmax: + disorder_constraints[Or([ + And([pre(r, act_y), ~delete(r, act_y), delete(r, act_x)]), + And([add(r, act_y), pre(r, act_x)]), + And([add(r, act_y), delete(r, act_x)]), + And([delete(r, act_y), add(r, act_x)]) + ])] = p * self.wmax + return disorder_constraints + + def _build_hard_parallel_constraints(self): + hard_constraints = {} + # create a list of all tuples + for act in self.actions: + for r in self.probabilities: + # for each action x proposition pair, enforce the two hard constraints with weight wmax + hard_constraints[implies(add(r, act), ~pre(r, act))] = self.wmax + hard_constraints[implies(delete(r, act), pre(r, act))] = self.wmax + return hard_constraints + + def _build_soft_parallel_constraints(self): + soft_constraints = {} # iterate through all parallel action sets for i in range(len(self.par_act_sets)): # within each parallel action set, iterate through the same action set again to compare @@ -95,56 +96,115 @@ def __build_soft_parallel_constraints(self): for act_x in self.par_act_sets[i]: for act_x_prime in self.par_act_sets[i]: if act_x != act_x_prime: + p = self.probabilities[ActionPair({act_x, act_x_prime})] # iterate through all propositions for r in self.propositions: # equivalent: if r is in the add or delete list of an action in the set, that implies it # can't be in the add or delete list of any other action in the set - constraints_4.add(implies(Or([add(r, act_x)], delete(r, act_x)), ~Or([add(r, act_x_prime)], delete(r, act_x_prime)))) - # TODO: enforce all [constraint 4] with weight (1 - p) x wmax + soft_constraints[implies(Or([add(r, act_x_prime)], delete(r, act_x_prime)), ~Or([add(r, act_x)], delete(r, act_x)))] = (1 - p) * self.wmax # then, iterate through all pairs of parallel action sets for i in range(len(self.par_act_sets) - 1): - # for each pair, compare every action in act_y to every action in act_x_prime; setting constraints - # assuming actions are disordered + # for each pair, compare every action in act_y to every action in act_x_prime; setting constraints assuming actions are disordered for act_y in self.par_act_sets[i + 1]: for act_x_prime in self.par_act_sets[i]: if act_y != act_x_prime: + p = self.probabilities[ActionPair({act_y, act_x_prime})] # iterate through all propositions and similarly set the constraint for r in self.propositions: - constraints_5.add(implies(Or([add(r, act_y)], delete(r, act_y)), ~Or([add(r, act_x_prime)], delete(r, act_x_prime)))) - # TODO: enforce all [constraint 5] with weight p x wmax - - def __build_parallel_constraints(self): - self.__build_hard_parallel_constraints() - self.__build_soft_parallel_constraints() + soft_constraints[implies(Or([add(r, act_x_prime)], delete(r, act_x_prime)), ~Or([add(r, act_y)], delete(r, act_y)))] = p * self.wmax + return soft_constraints + + def _build_parallel_constraints(self): + return self._build_hard_parallel_constraints() | self._build_soft_parallel_constraints() + + def _noise_constraints_6(self, obs_lists: ObservationLists): + noise_constraints_6 = {} + # set up dict + occurrences = {} + for a in self.actions: + occurrences[a] = {} + for r in self.propositions: + occurrences[a][r] = 0 + + # iterate over ALL the plan traces, adding occurrences accordingly + for i in range(len(obs_lists)): + # iterate through each step in each trace, omitting the last step because the last action is None/we access the state in the next step + for j in range(len(obs_lists[i]) - 1): + for r in obs_lists[i][j + 1].state.fluents: + # count the number of occurrences of each action and its following proposition + occurrences[obs_lists[i][j].action][r] += 1 + + # iterate through actions + for a in occurrences: + # iterate through all propositions for this action + for r in occurrences[a]: + occ_r = occurrences[a][r] + # if the # of occurrences is higher than the user-provided threshold: + if occ_r > self.occ_threshold: + # set constraint 6 with the calculated weight + # TODO: Ask - what "occurrences of all propositions" refers to - is it the total number of steps? total number of propositions? or something else? + noise_constraints_6[~delete(r, a)] = (occ_r / len(self.propositions)) + return noise_constraints_6 + + def _noise_constraints_7(self, obs_lists: ObservationLists): + noise_constraints_7 = {} + # iterate over ALL the plan traces, adding occurrences accordingly + for i in range(len(obs_lists)): + actions_taken = [] + # store the initial state s0 + s0 = obs_lists[i][0].state.fluents + # iterate through each step in each trace, omitting the last step because we access the state in the next step + for j in range(len(obs_lists[i]) - 1): + actions_taken.append(obs_lists[i][j].action) + # get all fluents in the state after the current action was taken + for r in obs_lists[i][j + 1].state.fluents: + # if r is not in s0, enforce constraint 7 with the calculated weight + if r not in s0: + noise_constraints_7[Or([add(r, act) for act in actions_taken])] = 1 # placeholder - def _build_noise_constraints(self): - # TODO: - # iterate through all tuples - # for each tuple: iterate through each step over ALL the plan traces - # count the number of occurrences; if higher than the user-provided parameter delta, - # store this tuple as a dictionary entry in a list of dictionaries (something like - # [{"action and proposition": , "occurrences of r:" 5}]). - # after all iterations are through, iterate through all the tuples in this dictionary, - # and set [constraint 6] with the calculated weight. - # TODO: Ask - what "occurrences of all propositions" refers to - is it the total number of steps...? - - # store the initial state s0 - # iterate through every step in the plan trace - # at each step, check all the propositions r in the current state - # if r is not in s0, enforce [constraint 7] with the calculated weight # TODO: Ask - what happens when you find the first r? I assume you keep iterating through the rest of the trace, # continuing the process with different propositions? Do we still count the occurrences of each proposition through - # the entire trace to use when we calculate the weight? - - # [constraint 8] is almost identical to [constraint 6]. Watch the order of the tuples. - - pass - - def _solve_constraints(self): - # TODO: - # call the MAXSAT solver - pass + # the entire trace to use when we calculate the weight? + + def _noise_constraints_8(self, obs_lists): + noise_constraints_8 = {} + # set up dict + occurrences = {} + for a in self.actions: + occurrences[a] = {} + for r in self.propositions: + occurrences[a][r] = 0 + + # iterate over ALL the plan traces, adding occurrences accordingly + for i in range(len(obs_lists)): + # iterate through each step in each trace + for j in range(len(obs_lists[i])): + for r in obs_lists[i][j].state.fluents: + # count the number of occurrences of each action and its previous proposition + occurrences[obs_lists[i][j].action][r] += 1 + + # iterate through actions + for a in occurrences: + # iterate through all propositions for this action + for r in occurrences[a]: + occ_r = occurrences[a][r] + # if the # of occurrences is higher than the user-provided threshold: + if occ_r > self.occ_threshold: + # set constraint 8 with the calculated weight + # TODO: Ask - what "occurrences of all propositions" refers to - is it the total number of steps? total number of propositions? or something else? + noise_constraints_8[pre(r, a)] = (occ_r / len(self.propositions)) + return noise_constraints_8 + + def _build_noise_constraints(self, obs_lists: ObservationLists): + return self._noise_constraints_6(obs_lists) | self._noise_constraints_7(obs_lists) | self._noise_constraints_8(obs_lists) + + def _set_all_constraints(self, obs_lists: ObservationLists): + return self._build_disorder_constraints() | self._build_parallel_constraints() | self._build_noise_constraints(obs_lists) + + def _solve_constraints(self, obs_lists: ObservationLists): + constraints = self._set_all_constraints(obs_lists) + # TODO: call the MAXSAT solver def _convert_to_model(self): # TODO: diff --git a/macq/trace/__init__.py b/macq/trace/__init__.py index 071c8f8a..2ae3c50a 100644 --- a/macq/trace/__init__.py +++ b/macq/trace/__init__.py @@ -6,7 +6,7 @@ from .trace import Trace, SAS from .trace_list import TraceList from .observation_lists import ObservationLists -from .disordered_parallel_actions_observation_lists import DisorderedParallelActionsObservationLists +from .disordered_parallel_actions_observation_lists import DisorderedParallelActionsObservationLists, ActionPair __all__ = [ @@ -20,5 +20,6 @@ "SAS", "TraceList", "ObservationLists", - "DisorderedParallelActionsObservationLists" + "DisorderedParallelActionsObservationLists", + "ActionPair" ] diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index 9cd51453..5625f479 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -39,6 +39,8 @@ def __init__(self, traces: TraceList, Token: Type[Observation] = NoisyPartialDis # dictionary that holds the probabilities of all actions being disordered self.probabilities = self._calculate_all_probabilities(f3_f10, f11_f40, learned_theta) + # will hold all the parallel action sets + self.par_act_sets = [] self.tokenize(traces, Token, **kwargs) def _decision(self, probability): @@ -140,6 +142,7 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): par_act_sets[i].add(act_y) par_act_sets[j].discard(act_y) par_act_sets[j].add(act_x) + self.par_act_sets.append(par_act_sets) tokens = [] for i in range(len(par_act_sets)): for act in par_act_sets[i]: From b04e2973e2a12df0299ea91a58a1f864e7b84eb8 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 11 Aug 2021 13:46:36 -0400 Subject: [PATCH 080/181] Change constraint breakpoint to make it more readable --- macq/extract/arms.py | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index f711f8f6..73729e0a 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -364,24 +364,24 @@ def implication(a: Var, b: Var): # relation in action.add => relation not in action.precond # relation in action.precond => relation not in action.add - # _BREAK_ marks unambiguous breakpoints for parsing later + # (BREAK) marks unambiguous breakpoints for parsing later constraints.append( implication( Var( - f"{relation.var()}_BREAK_in_BREAK_add_BREAK_{action.details()}" + f"{relation.var()} (BREAK) in (BREAK) add (BREAK) {action.details()}" ), Var( - f"{relation.var()}_BREAK_in_BREAK_pre_BREAK_{action.details()}" + f"{relation.var()} (BREAK) in (BREAK) pre (BREAK) {action.details()}" ).negate(), ) ) constraints.append( implication( Var( - f"{relation.var()}_BREAK_in_BREAK_pre_BREAK_{action.details()}" + f"{relation.var()} (BREAK) in (BREAK) pre (BREAK) {action.details()}" ), Var( - f"{relation.var()}_BREAK_in_BREAK_add_BREAK_{action.details()}" + f"{relation.var()} (BREAK) in (BREAK) add (BREAK) {action.details()}" ).negate(), ) ) @@ -391,10 +391,10 @@ def implication(a: Var, b: Var): constraints.append( implication( Var( - f"{relation.var()}_BREAK_in_BREAK_del_BREAK_{action.details()}" + f"{relation.var()} (BREAK) in (BREAK) del (BREAK) {action.details()}" ), Var( - f"{relation.var()}_BREAK_in_BREAK_pre_BREAK_{action.details()}" + f"{relation.var()} (BREAK) in (BREAK) pre (BREAK) {action.details()}" ), ) ) @@ -442,7 +442,7 @@ def _step2I( ai = actions[obs_i.action] i1.append( Var( - f"{relations[fluent].var()}_BREAK_in_BREAK_add_BREAK_{ai.details()}" + f"{relations[fluent].var()} (BREAK) in (BREAK) add (BREAK) {ai.details()}" ) ) @@ -452,7 +452,7 @@ def _step2I( a_n = obs_list[i - 1].action if a_n in actions and a_n is not None: i2 = Var( - f"{relations[fluent].var()}_BREAK_in_BREAK_del_BREAK_{actions[a_n].details()}" + f"{relations[fluent].var()} (BREAK) in (BREAK) del (BREAK) {actions[a_n].details()}" ).negate() if i1: @@ -472,7 +472,7 @@ def _step2I( Or( [ Var( - f"{relations[fluent].var()}_BREAK_in_BREAK_pre_BREAK_{actions[obs.action].details()}" + f"{relations[fluent].var()} (BREAK) in (BREAK) pre (BREAK) {actions[obs.action].details()}" ) ] ) @@ -483,7 +483,7 @@ def _step2I( Or( [ Var( - f"{relations[fluent].var()}_BREAK_in_BREAK_add_BREAK_{actions[a_n].details()}" + f"{relations[fluent].var()} (BREAK) in (BREAK) add (BREAK) {actions[a_n].details()}" ) ] ) @@ -576,7 +576,7 @@ def _step2P( relation_constraints.append( Var( - f"{relation.var()}_BREAK_relevant_BREAK_{ai.details()}_BREAK_{aj.details()}" + f"{relation.var()} (BREAK) relevant (BREAK) {ai.details()} (BREAK) {aj.details()}" ) ) if debug: @@ -658,6 +658,8 @@ def get_support_rate(count): @staticmethod def _step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: solver = RC2(max_sat) + + # solver. encoded_model = solver.compute() if not isinstance(encoded_model, list): @@ -686,7 +688,7 @@ def _step5( # models, however this is not a part of the paper and therefore not # implemented. for constraint, val in model.items(): - constraint = str(constraint).split("_BREAK_") + constraint = str(constraint).split(" (BREAK) ") relation = constraint[0] ctype = constraint[1] # constraint type if ctype == "in": @@ -729,11 +731,12 @@ def _step5( print(f"{relation} possibly explains action pair ({ai}, {aj})") for p, ai, aj in plan_constraints: - # one of the following must be true - if not ( - (p in ai.precond.intersection(aj.precond) and p not in ai.delete) # P3 - or (p in ai.add.intersection(aj.precond)) # P4 - or (p in ai.delete.intersection(aj.add)) # P5 + if ( + not ( + p in ai.precond.intersection(aj.precond) and p not in ai.delete + ) # P3 + or not (p in ai.add.intersection(aj.precond)) # P4 + or not (p in ai.delete.intersection(aj.add)) # P5 ): # check if either P3 or P4 are partially fulfilled and can be satisfied if p in ai.precond.union(aj.precond): From a507f445ed7edb0bab68a6bc289cc06c8492d79d Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 11 Aug 2021 13:46:59 -0400 Subject: [PATCH 081/181] Add ARMS testing --- macq/observation/observation.py | 4 +- macq/observation/partial_observation.py | 7 +++- tests/extract/test_arms.py | 55 +++++++++++++++++++++---- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/macq/observation/observation.py b/macq/observation/observation.py index d80817f5..73ca71a3 100644 --- a/macq/observation/observation.py +++ b/macq/observation/observation.py @@ -1,4 +1,4 @@ -from logging import warning +from logging import warn from json import dumps @@ -32,7 +32,7 @@ def __init__(self, **kwargs): if "index" in kwargs.keys(): self.index = kwargs["index"] else: - warning("Creating an Observation token without an index.") + warn("Creating an Observation token without an index.") def _matches(self, *_): raise NotImplementedError() diff --git a/macq/observation/partial_observation.py b/macq/observation/partial_observation.py index a54e98a1..28a470a6 100644 --- a/macq/observation/partial_observation.py +++ b/macq/observation/partial_observation.py @@ -1,4 +1,4 @@ -from logging import warning +from logging import warn from ..utils import PercentError from ..trace import Step, Fluent from ..trace import PartialState @@ -33,7 +33,7 @@ def __init__( raise PercentError() if percent_missing == 0 and not hide: - warning("Creating a PartialObseration with no missing information.") + warn("Creating a PartialObseration with no missing information.") super().__init__(index=step.index) @@ -105,6 +105,9 @@ def _matches(self, key: str, value: str): return value is None return self.action.details() == value elif key == "fluent_holds": + if self.state is None: + warn("Cannot query an empty state.") + return False return self.state.holds(value) else: raise InvalidQueryParameter(PartialObservation, key) diff --git a/tests/extract/test_arms.py b/tests/extract/test_arms.py index bd2d8817..236d6474 100644 --- a/tests/extract/test_arms.py +++ b/tests/extract/test_arms.py @@ -1,15 +1,54 @@ +from macq.trace import * from macq.extract import Extract, modes from macq.observation import PartialObservation -from tests.utils.generators import generate_blocks_traces +from macq.generate.pddl import * + + +def get_fluent(name: str, objs: list[str]): + objects = [PlanningObject(o.split()[0], o.split()[1]) for o in objs] + return Fluent(name, objects) if __name__ == "__main__": - traces = generate_blocks_traces(plan_len=10, num_traces=100) # need a goal - observations = traces.tokenize( - PartialObservation, - method="random", - percent_missing=0.10, + traces = TraceList() + generator = TraceFromGoal(problem_id=1801) + # for f in generator.trace.fluents: + # print(f) + + generator.change_goal( + { + get_fluent("communicated_soil_data", ["waypoint waypoint2"]), + get_fluent("communicated_rock_data", ["waypoint waypoint3"]), + get_fluent( + "communicated_image_data", ["objective objective1", "mode high_res"] + ), + } + ) + traces.append(generator.generate_trace()) + generator.change_goal( + { + get_fluent("communicated_soil_data", ["waypoint waypoint0"]), + get_fluent("communicated_rock_data", ["waypoint waypoint1"]), + get_fluent( + "communicated_image_data", ["objective objective1", "mode high_res"] + ), + } + ) + traces.append(generator.generate_trace()) + # traces.print("color") + + observations = traces.tokenize(PartialObservation, percent_missing=0.5) + # model = Extract(observations, modes.ARMS, upper_bound=2, debug=True) + model = Extract( + observations, + modes.ARMS, + debug=False, + upper_bound=5, + min_support=2, + action_weight=110, + info_weight=100, + threshold=0.66, + info3_default=30, + plan_default=30, ) - model = Extract(observations, modes.ARMS) - print() print(model.details()) From 12323621522535369b44a38fdcf7c8e9eda8464b Mon Sep 17 00:00:00 2001 From: beckydvn Date: Wed, 11 Aug 2021 15:12:03 -0400 Subject: [PATCH 082/181] update algorithms to iterate through all traces --- macq/extract/amdn.py | 128 ++++++++++-------- ...ered_parallel_actions_observation_lists.py | 4 +- tests/extract/test_amdn.py | 8 +- 3 files changed, 79 insertions(+), 61 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index cc4a47af..ae49c088 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -6,13 +6,13 @@ from ..observation import NoisyPartialDisorderedParallelObservation def __set_precond(r, act): - return Var(r + " is a precondition of " + act.name) + return Var(str(r) + " is a precondition of " + act.details()) def __set_del(r, act): - return Var(r + " is deleted by " + act.name) + return Var(str(r) + " is deleted by " + act.details()) def __set_add(r, act): - return Var(r + " is added by " + act.name) + return Var(str(r) + " is added by " + act.details()) # for easier reference pre = __set_precond @@ -40,42 +40,49 @@ def __init__(self, obs_lists: ObservationLists, wmax: float, occ_threshold: int) # get probability dictionary self.probabilities = obs_lists.probabilities # get parallel action sets - self.par_act_sets = obs_lists.par_act_sets - + self.all_par_act_sets = obs_lists.all_par_act_sets self.wmax = wmax self.occ_threshold = occ_threshold - #return Model(fluents, actions) + self._solve_constraints(obs_lists) + #return Model(fluents, actions) def _build_disorder_constraints(self): disorder_constraints = {} - # iterate through all pairs of parallel action sets - for i in range(len(self.par_act_sets) - 1): - # for each pair, iterate through all possible action combinations - for act_x in self.par_act_sets[i]: - for act_y in self.par_act_sets[i + 1]: - if act_x != act_y: - # calculate the probability of the actions being disordered (p) - p = self.probabilities[ActionPair({act_x, act_y})] - # for each action combination, iterate through all possible propositions - for r in self.propositions: - # enforce the following constraint if the actions are ordered with weight (1 - p) x wmax: - disorder_constraints[Or([ - And([pre(r, act_x), ~delete(r, act_x), delete(r, act_y)]), - And([add(r, act_x), pre(r, act_y)]), - And([add(r, act_x), delete(r, act_y)]), - And([delete(r, act_x), add(r, act_y)]) - ])] = (1 - p) * self.wmax - # likewise, enforce the following constraint if the actions are disordered with weight p x wmax: - disorder_constraints[Or([ - And([pre(r, act_y), ~delete(r, act_y), delete(r, act_x)]), - And([add(r, act_y), pre(r, act_x)]), - And([add(r, act_y), delete(r, act_x)]), - And([delete(r, act_y), add(r, act_x)]) - ])] = p * self.wmax - return disorder_constraints + # iterate through all traces + for i in range(len(self.all_par_act_sets)): + par_act_sets = self.all_par_act_sets[i] + # iterate through all pairs of parallel action sets for this trace + for j in range(len(par_act_sets) - 1): + # for each pair, iterate through all possible action combinations + for act_x in par_act_sets[j]: + for act_y in par_act_sets[j + 1]: + if act_x != act_y: + # calculate the probability of the actions being disordered (p) + p = self.probabilities[ActionPair({act_x, act_y})] + # for each action combination, iterate through all possible propositions + for r in self.propositions: + # enforce the following constraint if the actions are ordered with weight (1 - p) x wmax: + disorder_constraints[Or([ + And([pre(r, act_x), ~delete(r, act_x), delete(r, act_y)]), + And([add(r, act_x), pre(r, act_y)]), + And([add(r, act_x), delete(r, act_y)]), + And([delete(r, act_x), add(r, act_y)]) + ])] = (1 - p) * self.wmax + # likewise, enforce the following constraint if the actions are disordered with weight p x wmax: + disorder_constraints[Or([ + And([pre(r, act_y), ~delete(r, act_y), delete(r, act_x)]), + And([add(r, act_y), pre(r, act_x)]), + And([add(r, act_y), delete(r, act_x)]), + And([delete(r, act_y), add(r, act_x)]) + ])] = p * self.wmax + # for c, v in disorder_constraints.items(): + # print(c) + # print(v) + # print() + return disorder_constraints def _build_hard_parallel_constraints(self): hard_constraints = {} @@ -89,30 +96,36 @@ def _build_hard_parallel_constraints(self): def _build_soft_parallel_constraints(self): soft_constraints = {} - # iterate through all parallel action sets - for i in range(len(self.par_act_sets)): - # within each parallel action set, iterate through the same action set again to compare - # each action to every other action in the set; setting constraints assuming actions are not disordered - for act_x in self.par_act_sets[i]: - for act_x_prime in self.par_act_sets[i]: - if act_x != act_x_prime: - p = self.probabilities[ActionPair({act_x, act_x_prime})] - # iterate through all propositions - for r in self.propositions: - # equivalent: if r is in the add or delete list of an action in the set, that implies it - # can't be in the add or delete list of any other action in the set - soft_constraints[implies(Or([add(r, act_x_prime)], delete(r, act_x_prime)), ~Or([add(r, act_x)], delete(r, act_x)))] = (1 - p) * self.wmax - - # then, iterate through all pairs of parallel action sets - for i in range(len(self.par_act_sets) - 1): - # for each pair, compare every action in act_y to every action in act_x_prime; setting constraints assuming actions are disordered - for act_y in self.par_act_sets[i + 1]: - for act_x_prime in self.par_act_sets[i]: - if act_y != act_x_prime: - p = self.probabilities[ActionPair({act_y, act_x_prime})] - # iterate through all propositions and similarly set the constraint - for r in self.propositions: - soft_constraints[implies(Or([add(r, act_x_prime)], delete(r, act_x_prime)), ~Or([add(r, act_y)], delete(r, act_y)))] = p * self.wmax + # iterate through all traces + for i in range(len(self.all_par_act_sets)): + par_act_sets = self.all_par_act_sets[i] + # iterate through all parallel action sets for this trace + for j in range(len(par_act_sets)): + # within each parallel action set, iterate through the same action set again to compare + # each action to every other action in the set; setting constraints assuming actions are not disordered + for act_x in par_act_sets[j]: + for act_x_prime in par_act_sets[j]: + if act_x != act_x_prime: + p = self.probabilities[ActionPair({act_x, act_x_prime})] + # iterate through all propositions + for r in self.propositions: + # equivalent: if r is in the add or delete list of an action in the set, that implies it + # can't be in the add or delete list of any other action in the set + soft_constraints[implies(Or([add(r, act_x_prime)], delete(r, act_x_prime)), ~Or([add(r, act_x)], delete(r, act_x)))] = (1 - p) * self.wmax + + # iterate through all traces + for i in range(len(self.all_par_act_sets)): + par_act_sets = self.all_par_act_sets[i] + # then, iterate through all pairs of parallel action sets for each trace + for j in range(len(par_act_sets) - 1): + # for each pair, compare every action in act_y to every action in act_x_prime; setting constraints assuming actions are disordered + for act_y in par_act_sets[j + 1]: + for act_x_prime in par_act_sets[j]: + if act_y != act_x_prime: + p = self.probabilities[ActionPair({act_y, act_x_prime})] + # iterate through all propositions and similarly set the constraint + for r in self.propositions: + soft_constraints[implies(Or([add(r, act_x_prime)], delete(r, act_x_prime)), ~Or([add(r, act_y)], delete(r, act_y)))] = p * self.wmax return soft_constraints def _build_parallel_constraints(self): @@ -200,7 +213,10 @@ def _build_noise_constraints(self, obs_lists: ObservationLists): return self._noise_constraints_6(obs_lists) | self._noise_constraints_7(obs_lists) | self._noise_constraints_8(obs_lists) def _set_all_constraints(self, obs_lists: ObservationLists): - return self._build_disorder_constraints() | self._build_parallel_constraints() | self._build_noise_constraints(obs_lists) + #TODO: debug + dc_constraints = self._build_disorder_constraints() + parallel_constraints = self._build_parallel_constraints() + return dc_constraints | parallel_constraints | self._build_noise_constraints(obs_lists) def _solve_constraints(self, obs_lists: ObservationLists): constraints = self._set_all_constraints(obs_lists) diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index 5625f479..c8083c4c 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -40,7 +40,7 @@ def __init__(self, traces: TraceList, Token: Type[Observation] = NoisyPartialDis # dictionary that holds the probabilities of all actions being disordered self.probabilities = self._calculate_all_probabilities(f3_f10, f11_f40, learned_theta) # will hold all the parallel action sets - self.par_act_sets = [] + self.all_par_act_sets = [] self.tokenize(traces, Token, **kwargs) def _decision(self, probability): @@ -142,7 +142,7 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): par_act_sets[i].add(act_y) par_act_sets[j].discard(act_y) par_act_sets[j].add(act_x) - self.par_act_sets.append(par_act_sets) + self.all_par_act_sets.append(par_act_sets) tokens = [] for i in range(len(par_act_sets)): for act in par_act_sets[i]: diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 757c9b63..ada1e265 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -22,9 +22,11 @@ def test_tokenization_error(): # TODO: replace with a domain-specific random trace generator traces = RandomGoalSampling( - problem_id=2337, + #problem_id=2337, + dom=dom, + prob=prob, observe_pres_effs=True, - num_traces=1, + num_traces=3, steps_deep=10, subset_size_perc=0.1, enforced_hill_climbing_sampling=True @@ -37,5 +39,5 @@ def test_tokenization_error(): percent_noisy=0.05, ) print() - model = Extract(observations, modes.AMDN) + model = Extract(observations, modes.AMDN, wmax=100, occ_threshold=3) #print(model.details()) \ No newline at end of file From a9dc7f38b8cad59782c3b347ef8a4a2b66306ee1 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Wed, 11 Aug 2021 16:37:55 -0400 Subject: [PATCH 083/181] noise constraint bug fix --- macq/extract/amdn.py | 76 ++++++++++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index ae49c088..5484c4fa 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -46,6 +46,10 @@ def __init__(self, obs_lists: ObservationLists, wmax: float, occ_threshold: int) self._solve_constraints(obs_lists) + # TODO: make a function to calculate all occurrences of propositions for noise constraints. + # (the constraint-specific occurrences stored in the dict need to be unique because they take actions + # into account, but all_occ doesn't). + #return Model(fluents, actions) @@ -111,7 +115,7 @@ def _build_soft_parallel_constraints(self): for r in self.propositions: # equivalent: if r is in the add or delete list of an action in the set, that implies it # can't be in the add or delete list of any other action in the set - soft_constraints[implies(Or([add(r, act_x_prime)], delete(r, act_x_prime)), ~Or([add(r, act_x)], delete(r, act_x)))] = (1 - p) * self.wmax + soft_constraints[implies(Or([add(r, act_x_prime), delete(r, act_x_prime)]), Or([add(r, act_x), delete(r, act_x)]).negate())] = (1 - p) * self.wmax # iterate through all traces for i in range(len(self.all_par_act_sets)): @@ -125,28 +129,37 @@ def _build_soft_parallel_constraints(self): p = self.probabilities[ActionPair({act_y, act_x_prime})] # iterate through all propositions and similarly set the constraint for r in self.propositions: - soft_constraints[implies(Or([add(r, act_x_prime)], delete(r, act_x_prime)), ~Or([add(r, act_y)], delete(r, act_y)))] = p * self.wmax + soft_constraints[implies(Or([add(r, act_x_prime), delete(r, act_x_prime)]), Or([add(r, act_y), delete(r, act_y)]).negate())] = p * self.wmax return soft_constraints def _build_parallel_constraints(self): - return self._build_hard_parallel_constraints() | self._build_soft_parallel_constraints() + return {**self._build_hard_parallel_constraints(), **self._build_soft_parallel_constraints()} - def _noise_constraints_6(self, obs_lists: ObservationLists): - noise_constraints_6 = {} + def _set_up_occurrences_dict(self): # set up dict occurrences = {} for a in self.actions: occurrences[a] = {} for r in self.propositions: occurrences[a][r] = 0 + return occurrences + + def _noise_constraints_6(self, obs_lists: ObservationLists): + noise_constraints_6 = {} + # tracks occurrences of all propositions + all_occ = 0 + occurrences = self._set_up_occurrences_dict() # iterate over ALL the plan traces, adding occurrences accordingly for i in range(len(obs_lists)): # iterate through each step in each trace, omitting the last step because the last action is None/we access the state in the next step for j in range(len(obs_lists[i]) - 1): - for r in obs_lists[i][j + 1].state.fluents: + true_prop = [f for f in obs_lists[i][j + 1].state if obs_lists[i][j + 1].state[f]] + for r in true_prop: # count the number of occurrences of each action and its following proposition occurrences[obs_lists[i][j].action][r] += 1 + # TODO: fix bug: does not take last trace into account + all_occ += 1 # iterate through actions for a in occurrences: @@ -156,46 +169,61 @@ def _noise_constraints_6(self, obs_lists: ObservationLists): # if the # of occurrences is higher than the user-provided threshold: if occ_r > self.occ_threshold: # set constraint 6 with the calculated weight - # TODO: Ask - what "occurrences of all propositions" refers to - is it the total number of steps? total number of propositions? or something else? - noise_constraints_6[~delete(r, a)] = (occ_r / len(self.propositions)) + noise_constraints_6[~delete(r, a)] = (occ_r / all_occ) return noise_constraints_6 def _noise_constraints_7(self, obs_lists: ObservationLists): noise_constraints_7 = {} + # set up dict + occurrences = {} + for r in self.propositions: + occurrences[r] = 0 + # tracks occurrences of all propositions + all_occ = 0 + # track occurrences (used later for the weight). + # have to do this separately from the algorithm from the algorithm so everything is accounted for (#TODO: still true?) + for trace in obs_lists: + for step in trace: + true_prop = [f for f in step.state if step.state[f]] + for r in true_prop: + occurrences[r] += 1 + all_occ += 1 + # iterate over ALL the plan traces, adding occurrences accordingly for i in range(len(obs_lists)): actions_taken = [] # store the initial state s0 - s0 = obs_lists[i][0].state.fluents + s0 = obs_lists[i][0].state # iterate through each step in each trace, omitting the last step because we access the state in the next step for j in range(len(obs_lists[i]) - 1): actions_taken.append(obs_lists[i][j].action) + true_prop = [f for f in obs_lists[i][j + 1].state if obs_lists[i][j + 1].state[f]] # get all fluents in the state after the current action was taken - for r in obs_lists[i][j + 1].state.fluents: + for r in true_prop: # if r is not in s0, enforce constraint 7 with the calculated weight if r not in s0: - noise_constraints_7[Or([add(r, act) for act in actions_taken])] = 1 # placeholder + noise_constraints_7[Or([add(r, act) for act in actions_taken])] = occurrences[r]/all_occ # placeholder # TODO: Ask - what happens when you find the first r? I assume you keep iterating through the rest of the trace, # continuing the process with different propositions? Do we still count the occurrences of each proposition through - # the entire trace to use when we calculate the weight? + # the entire trace to use when we calculate the weight? + return noise_constraints_7 def _noise_constraints_8(self, obs_lists): + # tracks occurrences of all propositions + all_occ = 0 noise_constraints_8 = {} - # set up dict - occurrences = {} - for a in self.actions: - occurrences[a] = {} - for r in self.propositions: - occurrences[a][r] = 0 + occurrences = self._set_up_occurrences_dict() # iterate over ALL the plan traces, adding occurrences accordingly for i in range(len(obs_lists)): # iterate through each step in each trace for j in range(len(obs_lists[i])): - for r in obs_lists[i][j].state.fluents: + true_prop = [f for f in obs_lists[i][j].state if obs_lists[i][j].state[f]] + for r in true_prop: # count the number of occurrences of each action and its previous proposition occurrences[obs_lists[i][j].action][r] += 1 + all_occ += 1 # iterate through actions for a in occurrences: @@ -205,18 +233,18 @@ def _noise_constraints_8(self, obs_lists): # if the # of occurrences is higher than the user-provided threshold: if occ_r > self.occ_threshold: # set constraint 8 with the calculated weight - # TODO: Ask - what "occurrences of all propositions" refers to - is it the total number of steps? total number of propositions? or something else? - noise_constraints_8[pre(r, a)] = (occ_r / len(self.propositions)) + noise_constraints_8[pre(r, a)] = (occ_r / all_occ) return noise_constraints_8 def _build_noise_constraints(self, obs_lists: ObservationLists): - return self._noise_constraints_6(obs_lists) | self._noise_constraints_7(obs_lists) | self._noise_constraints_8(obs_lists) + return{**self._noise_constraints_6(obs_lists), **self._noise_constraints_7(obs_lists), **self._noise_constraints_8(obs_lists)} def _set_all_constraints(self, obs_lists: ObservationLists): #TODO: debug - dc_constraints = self._build_disorder_constraints() + disorder_constraints = self._build_disorder_constraints() parallel_constraints = self._build_parallel_constraints() - return dc_constraints | parallel_constraints | self._build_noise_constraints(obs_lists) + noise_constraints = self._build_noise_constraints(obs_lists) + return {**disorder_constraints, **parallel_constraints, **noise_constraints} def _solve_constraints(self, obs_lists: ObservationLists): constraints = self._set_all_constraints(obs_lists) From 7b13532543c31fde27545d6446c4f59c3ee36b25 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Thu, 12 Aug 2021 15:14:36 -0400 Subject: [PATCH 084/181] fix noise constraint 7 --- macq/extract/amdn.py | 136 ++++++++---------- ...ered_parallel_actions_observation_lists.py | 4 +- tests/extract/test_amdn.py | 2 +- 3 files changed, 61 insertions(+), 81 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 5484c4fa..19eef88d 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -19,9 +19,10 @@ def __set_add(r, act): add = __set_add delete = __set_del +WMAX = 100 + class AMDN: - # TODO: ask: wmax is a user value...? - def __init__(self, obs_lists: ObservationLists, wmax: float, occ_threshold: int): + def __init__(self, obs_lists: ObservationLists, occ_threshold: int): """Creates a new Model object. Args: @@ -37,27 +38,18 @@ def __init__(self, obs_lists: ObservationLists, wmax: float, occ_threshold: int) # create two base sets for all actions and propositions; store as attributes self.actions = obs_lists.actions self.propositions = {f for trace in obs_lists for step in trace for f in step.state.fluents} - # get probability dictionary - self.probabilities = obs_lists.probabilities - # get parallel action sets - self.all_par_act_sets = obs_lists.all_par_act_sets - self.wmax = wmax self.occ_threshold = occ_threshold self._solve_constraints(obs_lists) - - # TODO: make a function to calculate all occurrences of propositions for noise constraints. - # (the constraint-specific occurrences stored in the dict need to be unique because they take actions - # into account, but all_occ doesn't). - #return Model(fluents, actions) - def _build_disorder_constraints(self): + + def _build_disorder_constraints(self, obs_lists: ObservationLists): disorder_constraints = {} # iterate through all traces - for i in range(len(self.all_par_act_sets)): - par_act_sets = self.all_par_act_sets[i] + for i in range(len(obs_lists.all_par_act_sets)): + par_act_sets = obs_lists.all_par_act_sets[i] # iterate through all pairs of parallel action sets for this trace for j in range(len(par_act_sets) - 1): # for each pair, iterate through all possible action combinations @@ -65,7 +57,7 @@ def _build_disorder_constraints(self): for act_y in par_act_sets[j + 1]: if act_x != act_y: # calculate the probability of the actions being disordered (p) - p = self.probabilities[ActionPair({act_x, act_y})] + p = obs_lists.probabilities[ActionPair({act_x, act_y})] # for each action combination, iterate through all possible propositions for r in self.propositions: # enforce the following constraint if the actions are ordered with weight (1 - p) x wmax: @@ -74,35 +66,31 @@ def _build_disorder_constraints(self): And([add(r, act_x), pre(r, act_y)]), And([add(r, act_x), delete(r, act_y)]), And([delete(r, act_x), add(r, act_y)]) - ])] = (1 - p) * self.wmax + ])] = (1 - p) * WMAX # likewise, enforce the following constraint if the actions are disordered with weight p x wmax: disorder_constraints[Or([ And([pre(r, act_y), ~delete(r, act_y), delete(r, act_x)]), And([add(r, act_y), pre(r, act_x)]), And([add(r, act_y), delete(r, act_x)]), And([delete(r, act_y), add(r, act_x)]) - ])] = p * self.wmax - # for c, v in disorder_constraints.items(): - # print(c) - # print(v) - # print() + ])] = p * WMAX return disorder_constraints - def _build_hard_parallel_constraints(self): + def _build_hard_parallel_constraints(self, obs_lists: ObservationLists): hard_constraints = {} # create a list of all tuples for act in self.actions: - for r in self.probabilities: + for r in obs_lists.probabilities: # for each action x proposition pair, enforce the two hard constraints with weight wmax - hard_constraints[implies(add(r, act), ~pre(r, act))] = self.wmax - hard_constraints[implies(delete(r, act), pre(r, act))] = self.wmax + hard_constraints[implies(add(r, act), ~pre(r, act))] = WMAX + hard_constraints[implies(delete(r, act), pre(r, act))] = WMAX return hard_constraints - def _build_soft_parallel_constraints(self): + def _build_soft_parallel_constraints(self, obs_lists: ObservationLists): soft_constraints = {} # iterate through all traces - for i in range(len(self.all_par_act_sets)): - par_act_sets = self.all_par_act_sets[i] + for i in range(len(obs_lists.all_par_act_sets)): + par_act_sets = obs_lists.all_par_act_sets[i] # iterate through all parallel action sets for this trace for j in range(len(par_act_sets)): # within each parallel action set, iterate through the same action set again to compare @@ -110,30 +98,38 @@ def _build_soft_parallel_constraints(self): for act_x in par_act_sets[j]: for act_x_prime in par_act_sets[j]: if act_x != act_x_prime: - p = self.probabilities[ActionPair({act_x, act_x_prime})] + p = obs_lists.probabilities[ActionPair({act_x, act_x_prime})] # iterate through all propositions for r in self.propositions: # equivalent: if r is in the add or delete list of an action in the set, that implies it # can't be in the add or delete list of any other action in the set - soft_constraints[implies(Or([add(r, act_x_prime), delete(r, act_x_prime)]), Or([add(r, act_x), delete(r, act_x)]).negate())] = (1 - p) * self.wmax + soft_constraints[implies(Or([add(r, act_x_prime), delete(r, act_x_prime)]), Or([add(r, act_x), delete(r, act_x)]).negate())] = (1 - p) * WMAX # iterate through all traces - for i in range(len(self.all_par_act_sets)): - par_act_sets = self.all_par_act_sets[i] + for i in range(len(obs_lists.all_par_act_sets)): + par_act_sets = obs_lists.all_par_act_sets[i] # then, iterate through all pairs of parallel action sets for each trace for j in range(len(par_act_sets) - 1): # for each pair, compare every action in act_y to every action in act_x_prime; setting constraints assuming actions are disordered for act_y in par_act_sets[j + 1]: for act_x_prime in par_act_sets[j]: if act_y != act_x_prime: - p = self.probabilities[ActionPair({act_y, act_x_prime})] + p = obs_lists.probabilities[ActionPair({act_y, act_x_prime})] # iterate through all propositions and similarly set the constraint for r in self.propositions: - soft_constraints[implies(Or([add(r, act_x_prime), delete(r, act_x_prime)]), Or([add(r, act_y), delete(r, act_y)]).negate())] = p * self.wmax + soft_constraints[implies(Or([add(r, act_x_prime), delete(r, act_x_prime)]), Or([add(r, act_y), delete(r, act_y)]).negate())] = p * WMAX return soft_constraints - def _build_parallel_constraints(self): - return {**self._build_hard_parallel_constraints(), **self._build_soft_parallel_constraints()} + def _build_parallel_constraints(self, obs_lists: ObservationLists): + return {**self._build_hard_parallel_constraints(obs_lists), **self._build_soft_parallel_constraints(obs_lists)} + + def _calculate_all_r_occ(self, obs_lists: ObservationLists): + # tracks occurrences of all propositions + all_occ = 0 + for trace in obs_lists: + for step in trace: + all_occ += len([f for f in step.state if step.state[f]]) + return all_occ def _set_up_occurrences_dict(self): # set up dict @@ -143,11 +139,9 @@ def _set_up_occurrences_dict(self): for r in self.propositions: occurrences[a][r] = 0 return occurrences - - def _noise_constraints_6(self, obs_lists: ObservationLists): + + def _noise_constraints_6(self, obs_lists: ObservationLists, all_occ: int): noise_constraints_6 = {} - # tracks occurrences of all propositions - all_occ = 0 occurrences = self._set_up_occurrences_dict() # iterate over ALL the plan traces, adding occurrences accordingly @@ -158,8 +152,6 @@ def _noise_constraints_6(self, obs_lists: ObservationLists): for r in true_prop: # count the number of occurrences of each action and its following proposition occurrences[obs_lists[i][j].action][r] += 1 - # TODO: fix bug: does not take last trace into account - all_occ += 1 # iterate through actions for a in occurrences: @@ -172,46 +164,35 @@ def _noise_constraints_6(self, obs_lists: ObservationLists): noise_constraints_6[~delete(r, a)] = (occ_r / all_occ) return noise_constraints_6 - def _noise_constraints_7(self, obs_lists: ObservationLists): + def _noise_constraints_7(self, obs_lists: ObservationLists, all_occ: int): noise_constraints_7 = {} # set up dict occurrences = {} for r in self.propositions: occurrences[r] = 0 - # tracks occurrences of all propositions - all_occ = 0 - # track occurrences (used later for the weight). - # have to do this separately from the algorithm from the algorithm so everything is accounted for (#TODO: still true?) - for trace in obs_lists: - for step in trace: - true_prop = [f for f in step.state if step.state[f]] + + for trace in obs_lists.states: + for state in trace: + true_prop = [r for r in state if state[r]] for r in true_prop: occurrences[r] += 1 - all_occ += 1 - # iterate over ALL the plan traces, adding occurrences accordingly - for i in range(len(obs_lists)): - actions_taken = [] - # store the initial state s0 - s0 = obs_lists[i][0].state - # iterate through each step in each trace, omitting the last step because we access the state in the next step - for j in range(len(obs_lists[i]) - 1): - actions_taken.append(obs_lists[i][j].action) - true_prop = [f for f in obs_lists[i][j + 1].state if obs_lists[i][j + 1].state[f]] - # get all fluents in the state after the current action was taken + # iterate through all traces + for i in range(len(obs_lists.all_par_act_sets)): + # get the next trace/states + par_act_sets = obs_lists.all_par_act_sets[i] + states = obs_lists.states[i] + # iterate through all parallel action sets within the trace + for j in range(len(par_act_sets)): + # examine the states before and after each parallel action set; set constraints accordinglly + true_prop = [r for r in states[j + 1] if states[j + 1][r]] for r in true_prop: - # if r is not in s0, enforce constraint 7 with the calculated weight - if r not in s0: - noise_constraints_7[Or([add(r, act) for act in actions_taken])] = occurrences[r]/all_occ # placeholder - - # TODO: Ask - what happens when you find the first r? I assume you keep iterating through the rest of the trace, - # continuing the process with different propositions? Do we still count the occurrences of each proposition through - # the entire trace to use when we calculate the weight? + if not states[j][r]: + noise_constraints_7[Or([add(r, act) for act in par_act_sets[j]])] = occurrences[r]/all_occ + return noise_constraints_7 - def _noise_constraints_8(self, obs_lists): - # tracks occurrences of all propositions - all_occ = 0 + def _noise_constraints_8(self, obs_lists, all_occ: int): noise_constraints_8 = {} occurrences = self._set_up_occurrences_dict() @@ -223,7 +204,6 @@ def _noise_constraints_8(self, obs_lists): for r in true_prop: # count the number of occurrences of each action and its previous proposition occurrences[obs_lists[i][j].action][r] += 1 - all_occ += 1 # iterate through actions for a in occurrences: @@ -237,14 +217,12 @@ def _noise_constraints_8(self, obs_lists): return noise_constraints_8 def _build_noise_constraints(self, obs_lists: ObservationLists): - return{**self._noise_constraints_6(obs_lists), **self._noise_constraints_7(obs_lists), **self._noise_constraints_8(obs_lists)} + # calculate all occurrences for use in weights + all_occ = self._calculate_all_r_occ(obs_lists) + return{**self._noise_constraints_6(obs_lists, all_occ), **self._noise_constraints_7(obs_lists, all_occ), **self._noise_constraints_8(obs_lists, all_occ)} def _set_all_constraints(self, obs_lists: ObservationLists): - #TODO: debug - disorder_constraints = self._build_disorder_constraints() - parallel_constraints = self._build_parallel_constraints() - noise_constraints = self._build_noise_constraints(obs_lists) - return {**disorder_constraints, **parallel_constraints, **noise_constraints} + return {**self._build_disorder_constraints(obs_lists), ** self._build_parallel_constraints(obs_lists), **self._build_noise_constraints(obs_lists)} def _solve_constraints(self, obs_lists: ObservationLists): constraints = self._set_all_constraints(obs_lists) diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index c8083c4c..b6cf6dcf 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -39,8 +39,9 @@ def __init__(self, traces: TraceList, Token: Type[Observation] = NoisyPartialDis # dictionary that holds the probabilities of all actions being disordered self.probabilities = self._calculate_all_probabilities(f3_f10, f11_f40, learned_theta) - # will hold all the parallel action sets + # will hold all the parallel action sets and states self.all_par_act_sets = [] + self.states = [] self.tokenize(traces, Token, **kwargs) def _decision(self, probability): @@ -143,6 +144,7 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): par_act_sets[j].discard(act_y) par_act_sets[j].add(act_x) self.all_par_act_sets.append(par_act_sets) + self.states.append(states) tokens = [] for i in range(len(par_act_sets)): for act in par_act_sets[i]: diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index ada1e265..7c7378e0 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -39,5 +39,5 @@ def test_tokenization_error(): percent_noisy=0.05, ) print() - model = Extract(observations, modes.AMDN, wmax=100, occ_threshold=3) + model = Extract(observations, modes.AMDN, occ_threshold=3) #print(model.details()) \ No newline at end of file From 57679a6903559838adddf573d4b320c30e439351 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 12 Aug 2021 17:45:34 -0400 Subject: [PATCH 085/181] Add observation lists visualization --- macq/extract/arms.py | 19 +++- macq/observation/observation.py | 6 ++ macq/observation/partial_observation.py | 4 +- macq/trace/observation_lists.py | 129 +++++++++++++++++++++++- macq/trace/trace_list.py | 6 ++ macq/utils/pysat.py | 19 ++++ 6 files changed, 178 insertions(+), 5 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 73729e0a..a34bac6d 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -174,10 +174,10 @@ def _arms( list(action_map_rev.keys()), debug5, ) - if debug5: - input("Press enter to continue...") # Step 5 updates + + # Progress observed states if early actions have been learned setA = set() for action in action_map_rev.keys(): for i, obs_list in enumerate(obs_lists): @@ -186,6 +186,7 @@ def _arms( # update the next state with the effects and update the # early action pointer if obs_action in action_map and action == action_map[obs_action]: + print() # Set add effects true for add in action.add: # get candidate fluents from add relation @@ -236,6 +237,9 @@ def _arms( # Update Θ by adding A learned_actions.add(action) + if debug5: + input("Press enter to continue...") + return learned_actions @staticmethod @@ -553,6 +557,9 @@ def _step2P( ], min_support, ) + if debug: + print("Frequent pairs:") + print(frequent_pairs) # constraints: Dict[And[Or[Var]], int] = {} constraints: Dict[Or[Var], int] = {} @@ -657,9 +664,15 @@ def get_support_rate(count): @staticmethod def _step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: + from ..utils.pysat import H_DEL_PD, H_DEL_S, H_ADD_PU, O_DEL_PU + solver = RC2(max_sat) - # solver. + solver.add_clause([H_DEL_PD]) + solver.add_clause([H_DEL_S]) + solver.add_clause([H_ADD_PU]) + solver.add_clause([O_DEL_PU]) + encoded_model = solver.compute() if not isinstance(encoded_model, list): diff --git a/macq/observation/observation.py b/macq/observation/observation.py index 73ca71a3..a0d83208 100644 --- a/macq/observation/observation.py +++ b/macq/observation/observation.py @@ -1,5 +1,8 @@ from logging import warn from json import dumps +from typing import Union + +from ..trace import State, Action class InvalidQueryParameter(Exception): @@ -20,6 +23,9 @@ class Observation: The index of the associated step in the trace it is a part of. """ + state: State + action: Union[Action, None] + def __init__(self, **kwargs): """ Creates an Observation object, storing the step as a token, as well as its index/"place" diff --git a/macq/observation/partial_observation.py b/macq/observation/partial_observation.py index 28a470a6..9e846d81 100644 --- a/macq/observation/partial_observation.py +++ b/macq/observation/partial_observation.py @@ -1,9 +1,11 @@ from logging import warn + +from rich.console import Console from ..utils import PercentError from ..trace import Step, Fluent from ..trace import PartialState from . import Observation, InvalidQueryParameter -from typing import Set +from typing import Callable, Set import random diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index 6709e242..495eba13 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -1,4 +1,9 @@ -from typing import Iterator, List, Type, Set +from logging import warn +from typing import List, Type, Set +from inspect import cleandoc +from rich.console import Console +from rich.table import Table +from rich.text import Text from . import Trace from ..observation import Observation import macq.trace as TraceAPI @@ -68,3 +73,125 @@ def get_all_transitions(self): } except AttributeError: return {action: self.get_transitions(str(action)) for action in actions} + + def print(self, view="details", filter_func=lambda _: True, wrap=None): + """Pretty prints the trace list in the specified view. + + Arguments: + view ("details" | "color"): + Specifies the view format to print in. "details" provides a + detailed summary of each step in a trace. "color" provides a + color grid, mapping fluents in a step to either red or green + corresponding to the truth value. + filter_func (function): + Optional; Used to filter which fluents are printed in the + colorgrid display. + wrap (bool): + Determines if the output is wrapped or cut off. Details defaults + to cut off (wrap=False), color defaults to wrap (wrap=True). + """ + console = Console() + + views = ["details", "color"] + if view not in views: + warn(f'Invalid view {view}. Defaulting to "details".') + view = "details" + + obs_lists = [] + if view == "details": + if wrap is None: + wrap = False + obs_lists = [self._details(obs_list, wrap=wrap) for obs_list in self] + + elif view == "color": + if wrap is None: + wrap = True + obs_lists = [ + self._colorgrid(obs_list, filter_func=filter_func, wrap=wrap) + for obs_list in self + ] + + for obs_list in obs_lists: + console.print(obs_list) + print() + + @staticmethod + def _details(obs_list: List[Observation], wrap: bool): + indent = " " * 2 + # Summarize class attributes + details = Table.grid(expand=True) + details.title = "Trace" + details.add_column() + details.add_row( + cleandoc( + f""" + Attributes: + {indent}{len(obs_list)} steps + """ + ) + ) + steps = Table( + title="Steps", box=None, show_edge=False, pad_edge=False, expand=True + ) + steps.add_column("Step", justify="right", width=8) + steps.add_column( + "State", + justify="center", + overflow="ellipsis", + max_width=100, + no_wrap=(not wrap), + ) + steps.add_column("Action", overflow="ellipsis", no_wrap=(not wrap)) + + for obs in obs_list: + action = obs.action.details() if obs.action else "" + steps.add_row(str(obs.index), obs.state.details(), action) + + details.add_row(steps) + + return details + + @staticmethod + def _colorgrid(obs_list: List[Observation], filter_func: Callable, wrap: bool): + colorgrid = Table( + title="Trace", box=None, show_edge=False, pad_edge=False, expand=False + ) + colorgrid.add_column("Fluent", justify="right") + colorgrid.add_column( + header=Text("Step", justify="center"), overflow="fold", no_wrap=(not wrap) + ) + colorgrid.add_row( + "", + "".join( + [ + "|" if i < len(obs_list) and (i + 1) % 5 == 0 else " " + for i in range(len(obs_list)) + ] + ), + ) + + # TODO: get, sort, and filter fluents + + static = obs_list.get_static_fluents() + fluents = list( + filter( + filter_func, + sorted( + obs_list.fluents, + key=lambda f: float("inf") if f in static else len(str(f)), + ), + ) + ) + + for fluent in fluents: + step_str = "" + for obs in obs_list: + if obs.state[fluent]: + step_str += "[green]" + else: + step_str += "[red]" + step_str += "■" + + colorgrid.add_row(str(fluent), step_str) + + return colorgrid diff --git a/macq/trace/trace_list.py b/macq/trace/trace_list.py index a1252903..a8de2592 100644 --- a/macq/trace/trace_list.py +++ b/macq/trace/trace_list.py @@ -108,6 +108,12 @@ def print(self, view="details", filter_func=lambda _: True, wrap=None): detailed summary of each step in a trace. "color" provides a color grid, mapping fluents in a step to either red or green corresponding to the truth value. + filter_func (function): + Optional; Used to filter which fluents are printed in the + colorgrid display. + wrap (bool): + Determines if the output is wrapped or cut off. Details defaults + to cut off (wrap=False), color defaults to wrap (wrap=True). """ console = Console() diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py index ee554b48..05877d45 100644 --- a/macq/utils/pysat.py +++ b/macq/utils/pysat.py @@ -12,6 +12,25 @@ def __init__(self, clauses): def _encode(clauses: And[Or[Var]]) -> Tuple[List[List[int]], Dict[int, Hashable]]: decode = dict(enumerate(clauses.vars(), start=1)) encode = {v: k for k, v in decode.items()} + + # WARNING: debugging + for k in encode.keys(): + if "holding" in str(k) and "del" in str(k) and "put-down" in str(k): + global H_DEL_PD + H_DEL_PD = encode[k] + if "holding" in str(k) and "del" in str(k) and "(stack" in str(k): + print(k) + print(encode[k]) + global H_DEL_S + H_DEL_S = encode[k] + if "holding" in str(k) and "add" in str(k) and "pick-up" in str(k): + global H_ADD_PU + H_ADD_PU = encode[k] + if "on object" in str(k) and "del" in str(k) and "pick-up" in str(k): + global O_DEL_PU + O_DEL_PU = encode[k] + # WARNING: debugging + encoded = [ [encode[var.name] if var.true else -encode[var.name] for var in clause] for clause in clauses From f924dda70c24e0f03ba42ecafcd2dc250adc47e2 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Fri, 13 Aug 2021 10:30:03 -0400 Subject: [PATCH 086/181] import pysat --- macq/extract/amdn.py | 27 ++++++++++++++++++++++----- macq/utils/pysat.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 macq/utils/pysat.py diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 19eef88d..b13374f6 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -4,6 +4,7 @@ from .model import Model from ..trace import ObservationLists, ActionPair from ..observation import NoisyPartialDisorderedParallelObservation +from ..utils.pysat import to_wcnf def __set_precond(r, act): return Var(str(r) + " is a precondition of " + act.details()) @@ -40,7 +41,8 @@ def __init__(self, obs_lists: ObservationLists, occ_threshold: int): self.propositions = {f for trace in obs_lists for step in trace for f in step.state.fluents} self.occ_threshold = occ_threshold - self._solve_constraints(obs_lists) + solve = self._solve_constraints(obs_lists) + print() #return Model(fluents, actions) @@ -66,14 +68,19 @@ def _build_disorder_constraints(self, obs_lists: ObservationLists): And([add(r, act_x), pre(r, act_y)]), And([add(r, act_x), delete(r, act_y)]), And([delete(r, act_x), add(r, act_y)]) - ])] = (1 - p) * WMAX + ]).to_CNF()] = (1 - p) * WMAX # likewise, enforce the following constraint if the actions are disordered with weight p x wmax: disorder_constraints[Or([ And([pre(r, act_y), ~delete(r, act_y), delete(r, act_x)]), And([add(r, act_y), pre(r, act_x)]), And([add(r, act_y), delete(r, act_x)]), And([delete(r, act_y), add(r, act_x)]) - ])] = p * WMAX + ]).to_CNF()] = p * WMAX + # TODO: somehow convert to Or[Var] + # for c in disorder_constraints: + # if not c.is_CNF(): + # print("ERROR") + # print(And(c for c in disorder_constraints).to_CNF().is_CNF()) return disorder_constraints def _build_hard_parallel_constraints(self, obs_lists: ObservationLists): @@ -222,11 +229,21 @@ def _build_noise_constraints(self, obs_lists: ObservationLists): return{**self._noise_constraints_6(obs_lists, all_occ), **self._noise_constraints_7(obs_lists, all_occ), **self._noise_constraints_8(obs_lists, all_occ)} def _set_all_constraints(self, obs_lists: ObservationLists): - return {**self._build_disorder_constraints(obs_lists), ** self._build_parallel_constraints(obs_lists), **self._build_noise_constraints(obs_lists)} + return self._build_disorder_constraints(obs_lists) + #return {**self._build_disorder_constraints(obs_lists), ** self._build_parallel_constraints(obs_lists), **self._build_noise_constraints(obs_lists)} def _solve_constraints(self, obs_lists: ObservationLists): constraints = self._set_all_constraints(obs_lists) - # TODO: call the MAXSAT solver + problem = [] + constraints_ls : And[Or[Var]] = list(constraints.keys()) + for c in constraints_ls: + for f in c.children: + problem.append(f) + problem = And(problem) + print(problem.is_CNF()) + weights = list(constraints.values()) + wcnf, decode = to_wcnf(problem, weights) + return wcnf, decode def _convert_to_model(self): # TODO: diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py new file mode 100644 index 00000000..44201126 --- /dev/null +++ b/macq/utils/pysat.py @@ -0,0 +1,32 @@ +from typing import List, Tuple, Dict, Hashable +from pysat.formula import WCNF +from nnf import And, Or, Var + + +class NotCNF(Exception): + def __init__(self, clauses): + self.clauses = clauses + super().__init__(f"Cannot convert a non CNF formula to WCNF") + + +def _encode(clauses: And[Or[Var]]) -> Tuple[List[List[int]], Dict[int, Hashable]]: + decode = dict(enumerate(clauses.vars(), start=1)) + encode = {v: k for k, v in decode.items()} + encoded = [ + [encode[var.name] if var.true else -encode[var.name] for var in clause] + for clause in clauses + ] + + return encoded, decode + + +def to_wcnf( + clauses: And[Or[Var]], weights: List[int] +) -> Tuple[WCNF, Dict[int, Hashable]]: + """Converts a python-nnf CNF formula to a pysat WCNF.""" + # if not clauses.is_CNF(): + # raise NotCNF(clauses) + encoded, decode = _encode(clauses) + wcnf = WCNF() + wcnf.extend(encoded, weights) + return wcnf, decode \ No newline at end of file From 1412092a1b00db960f32034b570bae33dc85cbc0 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 13 Aug 2021 11:03:45 -0400 Subject: [PATCH 087/181] Add vizualization functions to ObservationLists --- macq/observation/observation.py | 10 ++- macq/trace/observation_lists.py | 149 +++++++++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 3 deletions(-) diff --git a/macq/observation/observation.py b/macq/observation/observation.py index d80817f5..a0d83208 100644 --- a/macq/observation/observation.py +++ b/macq/observation/observation.py @@ -1,5 +1,8 @@ -from logging import warning +from logging import warn from json import dumps +from typing import Union + +from ..trace import State, Action class InvalidQueryParameter(Exception): @@ -20,6 +23,9 @@ class Observation: The index of the associated step in the trace it is a part of. """ + state: State + action: Union[Action, None] + def __init__(self, **kwargs): """ Creates an Observation object, storing the step as a token, as well as its index/"place" @@ -32,7 +38,7 @@ def __init__(self, **kwargs): if "index" in kwargs.keys(): self.index = kwargs["index"] else: - warning("Creating an Observation token without an index.") + warn("Creating an Observation token without an index.") def _matches(self, *_): raise NotImplementedError() diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index 13b02c1e..2e956052 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -1,4 +1,10 @@ -from typing import List, Type, Set +from collections import defaultdict +from logging import warn +from typing import Callable, List, Type, Set +from inspect import cleandoc +from rich.console import Console +from rich.table import Table +from rich.text import Text from . import Trace from ..observation import Observation import macq.trace as TraceAPI @@ -59,3 +65,144 @@ def get_all_transitions(self): } except AttributeError: return {action: self.get_transitions(str(action)) for action in actions} + + def print(self, view="details", filter_func=lambda _: True, wrap=None): + """Pretty prints the trace list in the specified view. + + Arguments: + view ("details" | "color"): + Specifies the view format to print in. "details" provides a + detailed summary of each step in a trace. "color" provides a + color grid, mapping fluents in a step to either red or green + corresponding to the truth value. + filter_func (function): + Optional; Used to filter which fluents are printed in the + colorgrid display. + wrap (bool): + Determines if the output is wrapped or cut off. Details defaults + to cut off (wrap=False), color defaults to wrap (wrap=True). + """ + console = Console() + + views = ["details", "color"] + if view not in views: + warn(f'Invalid view {view}. Defaulting to "details".') + view = "details" + + obs_lists = [] + if view == "details": + if wrap is None: + wrap = False + obs_lists = [self._details(obs_list, wrap=wrap) for obs_list in self] + + elif view == "color": + if wrap is None: + wrap = True + obs_lists = [ + self._colorgrid(obs_list, filter_func=filter_func, wrap=wrap) + for obs_list in self + ] + + for obs_list in obs_lists: + console.print(obs_list) + print() + + def _details(self, obs_list: List[Observation], wrap: bool): + indent = " " * 2 + # Summarize class attributes + details = Table.grid(expand=True) + details.title = "Trace" + details.add_column() + details.add_row( + cleandoc( + f""" + Attributes: + {indent}{len(obs_list)} steps + {indent}{len(self.get_fluents())} fluents + """ + ) + ) + steps = Table( + title="Steps", box=None, show_edge=False, pad_edge=False, expand=True + ) + steps.add_column("Step", justify="right", width=8) + steps.add_column( + "State", + justify="center", + overflow="ellipsis", + max_width=100, + no_wrap=(not wrap), + ) + steps.add_column("Action", overflow="ellipsis", no_wrap=(not wrap)) + + for obs in obs_list: + action = obs.action.details() if obs.action else "" + steps.add_row(str(obs.index), obs.state.details(), action) + + details.add_row(steps) + + return details + + @staticmethod + def _colorgrid(obs_list: List[Observation], filter_func: Callable, wrap: bool): + colorgrid = Table( + title="Trace", box=None, show_edge=False, pad_edge=False, expand=False + ) + colorgrid.add_column("Fluent", justify="right") + colorgrid.add_column( + header=Text("Step", justify="center"), overflow="fold", no_wrap=(not wrap) + ) + colorgrid.add_row( + "", + "".join( + [ + "|" if i < len(obs_list) and (i + 1) % 5 == 0 else " " + for i in range(len(obs_list)) + ] + ), + ) + + static = ObservationLists.get_obs_static_fluents(obs_list) + fluents = list( + filter( + filter_func, + sorted( + ObservationLists.get_obs_fluents(obs_list), + key=lambda f: float("inf") if f in static else len(str(f)), + ), + ) + ) + + for fluent in fluents: + step_str = "" + for obs in obs_list: + if obs.state[fluent]: + step_str += "[green]" + else: + step_str += "[red]" + step_str += "■" + + colorgrid.add_row(str(fluent), step_str) + + return colorgrid + + @staticmethod + def get_obs_fluents(obs_list: List[Observation]): + fluents = set() + for obs in obs_list: + fluents.update(list(obs.state.keys())) + return fluents + + @staticmethod + def get_obs_static_fluents(obs_list: List[Observation]): + fstates = defaultdict(list) + for obs in obs_list: + for f, v in obs.state.items(): + fstates[f].append(v) + + static = set() + for f, states in fstates.items(): + if all(states) or not any(states): + static.add(f) + + return static From 25b05c30beb1367982f1318654d969eb85c9a71b Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 13 Aug 2021 12:31:00 -0400 Subject: [PATCH 088/181] Add SAS triples to ObservationLists --- macq/trace/observation_lists.py | 24 +++++++++++++++++++++++- macq/trace/trace.py | 6 +++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index 2e956052..a8ae9122 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -5,7 +5,7 @@ from rich.console import Console from rich.table import Table from rich.text import Text -from . import Trace +from . import Trace, Action, SAS from ..observation import Observation import macq.trace as TraceAPI @@ -206,3 +206,25 @@ def get_obs_static_fluents(obs_list: List[Observation]): static.add(f) return static + + def get_sas_triples(self, action: Action) -> List[SAS]: + """Retrieves the list of (S,A,S') triples for the action in this trace. + + In a (S,A,S') triple, S is the pre-state, A is the action, and S' is + the post-state. + + Args: + action (Action): + The action to retrieve (S,A,S') triples for. + + Returns: + A `SAS` object, containing the `pre_state`, `action`, and + `post_state`. + """ + sas_triples = [] + for obs_list in self: + for i, obs in enumerate(obs_list): + if obs.action == action: + triple = SAS(obs.state, action, self[i + 1].state) + sas_triples.append(triple) + return sas_triples diff --git a/macq/trace/trace.py b/macq/trace/trace.py index 53a80c75..27700c13 100644 --- a/macq/trace/trace.py +++ b/macq/trace/trace.py @@ -261,7 +261,7 @@ def get_post_states(self, action: Action): post_states.add(self[i + 1].state) return post_states - def get_sas_triples(self, action: Action) -> Set[SAS]: + def get_sas_triples(self, action: Action) -> List[SAS]: """Retrieves the list of (S,A,S') triples for the action in this trace. In a (S,A,S') triple, S is the pre-state, A is the action, and S' is @@ -275,11 +275,11 @@ def get_sas_triples(self, action: Action) -> Set[SAS]: A `SAS` object, containing the `pre_state`, `action`, and `post_state`. """ - sas_triples = set() + sas_triples = [] for i, step in enumerate(self): if step.action == action: triple = SAS(step.state, action, self[i + 1].state) - sas_triples.add(triple) + sas_triples.append(triple) return sas_triples def get_total_cost(self): From 1e731ac1ff1e5467e13fd5958707c9d12749f843 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 13 Aug 2021 13:05:25 -0400 Subject: [PATCH 089/181] Remove SAS triples from ObservationLists (use get_transitions instead) --- macq/observation/observation.py | 2 +- macq/trace/observation_lists.py | 22 ---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/macq/observation/observation.py b/macq/observation/observation.py index a0d83208..aa357d6d 100644 --- a/macq/observation/observation.py +++ b/macq/observation/observation.py @@ -23,7 +23,7 @@ class Observation: The index of the associated step in the trace it is a part of. """ - state: State + state: Union[State, None] action: Union[Action, None] def __init__(self, **kwargs): diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index 00e000c1..8f728909 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -215,25 +215,3 @@ def get_obs_static_fluents(obs_list: List[Observation]): static.add(f) return static - - def get_sas_triples(self, action: Action) -> List[SAS]: - """Retrieves the list of (S,A,S') triples for the action in this trace. - - In a (S,A,S') triple, S is the pre-state, A is the action, and S' is - the post-state. - - Args: - action (Action): - The action to retrieve (S,A,S') triples for. - - Returns: - A `SAS` object, containing the `pre_state`, `action`, and - `post_state`. - """ - sas_triples = [] - for obs_list in self: - for i, obs in enumerate(obs_list): - if obs.action == action: - triple = SAS(obs.state, action, self[i + 1].state) - sas_triples.append(triple) - return sas_triples From 506df42a1426101866655af2f85fb2f84f1d992f Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 13 Aug 2021 13:26:34 -0400 Subject: [PATCH 090/181] Make all observations hashable --- macq/observation/identity_observation.py | 10 +++------- macq/observation/observation.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/macq/observation/identity_observation.py b/macq/observation/identity_observation.py index 16b3dd5c..c119d396 100644 --- a/macq/observation/identity_observation.py +++ b/macq/observation/identity_observation.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from typing import Optional, List -from ..trace import Step +from ..trace import Step, State from . import Observation, InvalidQueryParameter @@ -11,6 +11,8 @@ class IdentityObservation(Observation): class. """ + state: State + class IdentityState(dict): def __hash__(self): return hash(tuple(sorted(self.items()))) @@ -41,17 +43,11 @@ def __init__(self, step: Step, **kwargs): self.state = step.state.clone() self.action = None if step.action is None else step.action.clone() - def __hash__(self): - return hash(self.details()) - def __eq__(self, other): if not isinstance(other, IdentityObservation): return False return self.state == other.state and self.action == other.action - def details(self): - return f"Obs {str(self.index)}.\n State: {str(self.state)}\n Action: {str(self.action)}" - def _matches(self, key: str, value: str): if key == "action": if self.action is None: diff --git a/macq/observation/observation.py b/macq/observation/observation.py index aa357d6d..4645fed7 100644 --- a/macq/observation/observation.py +++ b/macq/observation/observation.py @@ -23,6 +23,7 @@ class Observation: The index of the associated step in the trace it is a part of. """ + index: int state: Union[State, None] action: Union[Action, None] @@ -40,6 +41,20 @@ def __init__(self, **kwargs): else: warn("Creating an Observation token without an index.") + def __hash__(self): + return hash(self.details()) + + def details(self): + out = "Observation\n" + if self.index is not None: + out += f" Index: {str(self.index)}\n" + if self.state: + out += f" State: {str(self.state)}\n" + if self.action: + out += f" Action: {str(self.action)}\n" + + return out + def _matches(self, *_): raise NotImplementedError() From 5f0dbc547467b9033dcd43befd89e703a26231dc Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 13 Aug 2021 13:34:39 -0400 Subject: [PATCH 091/181] Add warning for generic Observation hashes --- macq/observation/observation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/macq/observation/observation.py b/macq/observation/observation.py index 4645fed7..c6a92366 100644 --- a/macq/observation/observation.py +++ b/macq/observation/observation.py @@ -42,7 +42,10 @@ def __init__(self, **kwargs): warn("Creating an Observation token without an index.") def __hash__(self): - return hash(self.details()) + string = self.details() + if string == "Observation\n": + warn("Observation has no unique information. Generating a generic hash.") + return hash(string) def details(self): out = "Observation\n" From 2dc7a1a44b01edacbade3e3c81c1c9d1414a3089 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 13 Aug 2021 13:39:04 -0400 Subject: [PATCH 092/181] Fix type errors --- macq/observation/identity_observation.py | 3 +++ macq/observation/partial_observation.py | 3 +++ macq/trace/observation_lists.py | 13 ++++++++----- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/macq/observation/identity_observation.py b/macq/observation/identity_observation.py index c119d396..f8e8ca29 100644 --- a/macq/observation/identity_observation.py +++ b/macq/observation/identity_observation.py @@ -43,6 +43,9 @@ def __init__(self, step: Step, **kwargs): self.state = step.state.clone() self.action = None if step.action is None else step.action.clone() + def __hash__(self): + return super().__hash__() + def __eq__(self, other): if not isinstance(other, IdentityObservation): return False diff --git a/macq/observation/partial_observation.py b/macq/observation/partial_observation.py index 9e846d81..f4285756 100644 --- a/macq/observation/partial_observation.py +++ b/macq/observation/partial_observation.py @@ -51,6 +51,9 @@ def __init__( self.state = None if percent_missing == 1 else step.state.clone() self.action = None if step.action is None else step.action.clone() + def __hash__(self): + return super().__hash__() + def __eq__(self, other): return ( isinstance(other, PartialObservation) diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index 8f728909..638420a4 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -146,7 +146,8 @@ def _details(self, obs_list: List[Observation], wrap: bool): for obs in obs_list: action = obs.action.details() if obs.action else "" - steps.add_row(str(obs.index), obs.state.details(), action) + state = obs.state.details() if obs.state else "n/a" + steps.add_row(str(obs.index), state, action) details.add_row(steps) @@ -185,7 +186,7 @@ def _colorgrid(obs_list: List[Observation], filter_func: Callable, wrap: bool): for fluent in fluents: step_str = "" for obs in obs_list: - if obs.state[fluent]: + if obs.state and obs.state[fluent]: step_str += "[green]" else: step_str += "[red]" @@ -199,15 +200,17 @@ def _colorgrid(obs_list: List[Observation], filter_func: Callable, wrap: bool): def get_obs_fluents(obs_list: List[Observation]): fluents = set() for obs in obs_list: - fluents.update(list(obs.state.keys())) + if obs.state: + fluents.update(list(obs.state.keys())) return fluents @staticmethod def get_obs_static_fluents(obs_list: List[Observation]): fstates = defaultdict(list) for obs in obs_list: - for f, v in obs.state.items(): - fstates[f].append(v) + if obs.state: + for f, v in obs.state.items(): + fstates[f].append(v) static = set() for f, states in fstates.items(): From ad8a48104825060b27de702af3fc243aa495da69 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 13 Aug 2021 15:06:08 -0400 Subject: [PATCH 093/181] Fix typing on ObservationLists functions --- macq/trace/observation_lists.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index 638420a4..3dfc24a5 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -1,6 +1,6 @@ from collections import defaultdict from logging import warn -from typing import Callable, List, Type, Set +from typing import Callable, Dict, List, Type, Set, overload from inspect import cleandoc from rich.console import Console from rich.table import Table @@ -41,8 +41,8 @@ def get_fluents(self): fluents.update(list(obs.state.keys())) return fluents - def fetch_observations(self, query: dict): - matches: List[Set[Observation]] = list() + def fetch_observations(self, query: dict) -> List[Set[Observation]]: + matches: List[Set[Observation]] = [] trace: List[Observation] for i, trace in enumerate(self): matches.append(set()) @@ -51,22 +51,24 @@ def fetch_observations(self, query: dict): matches[i].add(obs) return matches # list of sets of matching fluents from each trace - def fetch_observation_windows(self, query: dict, left: int, right: int): + def fetch_observation_windows( + self, query: dict, left: int, right: int + ) -> List[List[Observation]]: windows = [] matches = self.fetch_observations(query) - trace: Set[Observation] - for i, trace in enumerate(matches): # note obs.index starts at 1 (index = i+1) - for obs in trace: + # note obs.index starts at 1 (index = i+1) + for i, obs_set in enumerate(matches): + for obs in obs_set: start = obs.index - left - 1 end = obs.index + right windows.append(self[i][start:end]) return windows - def get_transitions(self, action: str): + def get_transitions(self, action: str) -> List[List[Observation]]: query = {"action": action} return self.fetch_observation_windows(query, 0, 1) - def get_all_transitions(self): + def get_all_transitions(self) -> Dict[Action, List[List[Observation]]]: actions = self.get_actions() try: return { @@ -145,9 +147,8 @@ def _details(self, obs_list: List[Observation], wrap: bool): steps.add_column("Action", overflow="ellipsis", no_wrap=(not wrap)) for obs in obs_list: - action = obs.action.details() if obs.action else "" - state = obs.state.details() if obs.state else "n/a" - steps.add_row(str(obs.index), state, action) + ind, state, action = obs.get_details() + steps.add_row(ind, state, action) details.add_row(steps) From 2ba61414952a03bf38aaa308389f28d78ee84890 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 13 Aug 2021 15:06:29 -0400 Subject: [PATCH 094/181] Move detail generation to Observation --- macq/observation/observation.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/macq/observation/observation.py b/macq/observation/observation.py index c6a92366..a48ab6ce 100644 --- a/macq/observation/observation.py +++ b/macq/observation/observation.py @@ -42,12 +42,12 @@ def __init__(self, **kwargs): warn("Creating an Observation token without an index.") def __hash__(self): - string = self.details() + string = str(self) if string == "Observation\n": warn("Observation has no unique information. Generating a generic hash.") return hash(string) - def details(self): + def __str__(self): out = "Observation\n" if self.index is not None: out += f" Index: {str(self.index)}\n" @@ -58,6 +58,12 @@ def details(self): return out + def get_details(self): + ind = str(self.index) if self.index else "-" + state = self.state.details() if self.state else "-" + action = self.action.details() if self.action else "" + return (ind, state, action) + def _matches(self, *_): raise NotImplementedError() From 6521253524ce5a3dfa4154386ffeec5f9f493846 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 13 Aug 2021 16:06:38 -0400 Subject: [PATCH 095/181] Overload ObservationLists constructor --- macq/trace/observation_lists.py | 35 +++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index 3dfc24a5..4a175b4e 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -1,6 +1,6 @@ from collections import defaultdict from logging import warn -from typing import Callable, Dict, List, Type, Set, overload +from typing import Callable, Dict, List, Type, Set, Union from inspect import cleandoc from rich.console import Console from rich.table import Table @@ -10,6 +10,15 @@ import macq.trace as TraceAPI +class MissingToken(Exception): + def __init__(self, message=None): + if message is None: + message = ( + f"Cannot create ObservationLists from a TraceList without a Token." + ) + super().__init__(message) + + class ObservationLists(TraceAPI.TraceList): traces: List[List[Observation]] # Disable methods @@ -17,13 +26,23 @@ class ObservationLists(TraceAPI.TraceList): get_usage = property() tokenize = property() - def __init__(self, traces: TraceAPI.TraceList, Token: Type[Observation], **kwargs): - self.traces = [] - self.type = Token - trace: Trace - for trace in traces: - tokens = trace.tokenize(Token, **kwargs) - self.append(tokens) + def __init__( + self, + traces: Union[TraceAPI.TraceList, List[List[Observation]]], + Token: Type[Observation] = None, + **kwargs, + ): + if isinstance(traces, TraceAPI.TraceList): + if not Token: + raise MissingToken() + self.traces = [] + self.type = Token + trace: Trace + for trace in traces: + tokens = trace.tokenize(Token, **kwargs) + self.append(tokens) + else: + self.traces = traces def get_actions(self): actions = set() From dd7162d1dce0522fa8faa895dbb4f4f90c7ada7c Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 13 Aug 2021 16:23:20 -0400 Subject: [PATCH 096/181] Remove debugging code --- macq/extract/arms.py | 7 ------- macq/utils/pysat.py | 18 ------------------ 2 files changed, 25 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index a34bac6d..f707121b 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -664,15 +664,8 @@ def get_support_rate(count): @staticmethod def _step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: - from ..utils.pysat import H_DEL_PD, H_DEL_S, H_ADD_PU, O_DEL_PU - solver = RC2(max_sat) - solver.add_clause([H_DEL_PD]) - solver.add_clause([H_DEL_S]) - solver.add_clause([H_ADD_PU]) - solver.add_clause([O_DEL_PU]) - encoded_model = solver.compute() if not isinstance(encoded_model, list): diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py index 05877d45..74be5df7 100644 --- a/macq/utils/pysat.py +++ b/macq/utils/pysat.py @@ -13,24 +13,6 @@ def _encode(clauses: And[Or[Var]]) -> Tuple[List[List[int]], Dict[int, Hashable] decode = dict(enumerate(clauses.vars(), start=1)) encode = {v: k for k, v in decode.items()} - # WARNING: debugging - for k in encode.keys(): - if "holding" in str(k) and "del" in str(k) and "put-down" in str(k): - global H_DEL_PD - H_DEL_PD = encode[k] - if "holding" in str(k) and "del" in str(k) and "(stack" in str(k): - print(k) - print(encode[k]) - global H_DEL_S - H_DEL_S = encode[k] - if "holding" in str(k) and "add" in str(k) and "pick-up" in str(k): - global H_ADD_PU - H_ADD_PU = encode[k] - if "on object" in str(k) and "del" in str(k) and "pick-up" in str(k): - global O_DEL_PU - O_DEL_PU = encode[k] - # WARNING: debugging - encoded = [ [encode[var.name] if var.true else -encode[var.name] for var in clause] for clause in clauses From 5e45a675587e230fc2e5bcd6442efe4545f63f39 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Mon, 16 Aug 2021 11:52:06 -0400 Subject: [PATCH 097/181] fix disorder constraints weight, make amdn cls --- macq/extract/amdn.py | 139 +++++++++++------- ...ered_parallel_actions_observation_lists.py | 2 + tests/extract/test_amdn.py | 1 + 3 files changed, 89 insertions(+), 53 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index b13374f6..4c3f77f7 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -1,29 +1,30 @@ from nnf.operators import implies import macq.extract as extract -from nnf import Var, And, Or +from typing import Dict, Union, Set +from nnf import Aux, Var, And, Or from .model import Model -from ..trace import ObservationLists, ActionPair +from ..trace import ObservationLists, ActionPair, Fluent # for typing from ..observation import NoisyPartialDisorderedParallelObservation from ..utils.pysat import to_wcnf def __set_precond(r, act): - return Var(str(r) + " is a precondition of " + act.details()) + return Var(str(r)[1:-1] + " is a precondition of " + act.details()) def __set_del(r, act): - return Var(str(r) + " is deleted by " + act.details()) + return Var(str(r)[1:-1] + " is deleted by " + act.details()) def __set_add(r, act): - return Var(str(r) + " is added by " + act.details()) + return Var(str(r)[1:-1] + " is added by " + act.details()) # for easier reference pre = __set_precond add = __set_add delete = __set_del -WMAX = 100 +WMAX = 1 class AMDN: - def __init__(self, obs_lists: ObservationLists, occ_threshold: int): + def __new__(cls, obs_lists: ObservationLists, occ_threshold: int): """Creates a new Model object. Args: @@ -36,18 +37,39 @@ def __init__(self, obs_lists: ObservationLists, occ_threshold: int): if obs_lists.type is not NoisyPartialDisorderedParallelObservation: raise extract.IncompatibleObservationToken(obs_lists.type, AMDN) - # create two base sets for all actions and propositions; store as attributes - self.actions = obs_lists.actions - self.propositions = {f for trace in obs_lists for step in trace for f in step.state.fluents} - self.occ_threshold = occ_threshold - - solve = self._solve_constraints(obs_lists) + solve = AMDN._solve_constraints(obs_lists, occ_threshold) print() #return Model(fluents, actions) + @staticmethod + def _or_refactor(maybe_lit: Union[Or, Var]): + """Converts a "Var" fluent to an "Or" fluent. + + Args: + maybe_lit (Union[Or, Var]): + Fluent that is either type "Or" or "Var." + + Returns: + A corresponding fluent of type "Or." + """ + return Or([maybe_lit]) if isinstance(maybe_lit, Var) or isinstance(maybe_lit, Aux) else maybe_lit + @staticmethod + def _set_disorder_constraint_weights(cnf_formula: And[Or[Var]], disorder_constraints: Dict, prob_disordered: float): + aux_var = set() + # find all the auxiliary variables + for clause in cnf_formula.children: + for var in clause.children: + if isinstance(var.name, Aux): + aux_var.add(var.name) + # set each original constraint to be a hard clause + disorder_constraints[clause] = "HARD" + # aux variables are the soft clauses that get the original weight + for aux in aux_var: + disorder_constraints[AMDN._or_refactor(aux)] = prob_disordered * WMAX - def _build_disorder_constraints(self, obs_lists: ObservationLists): + @staticmethod + def _build_disorder_constraints(obs_lists: ObservationLists): disorder_constraints = {} # iterate through all traces for i in range(len(obs_lists.all_par_act_sets)): @@ -61,39 +83,39 @@ def _build_disorder_constraints(self, obs_lists: ObservationLists): # calculate the probability of the actions being disordered (p) p = obs_lists.probabilities[ActionPair({act_x, act_y})] # for each action combination, iterate through all possible propositions - for r in self.propositions: + for r in obs_lists.propositions: # enforce the following constraint if the actions are ordered with weight (1 - p) x wmax: - disorder_constraints[Or([ + constraint_1 = Or([ And([pre(r, act_x), ~delete(r, act_x), delete(r, act_y)]), And([add(r, act_x), pre(r, act_y)]), And([add(r, act_x), delete(r, act_y)]), And([delete(r, act_x), add(r, act_y)]) - ]).to_CNF()] = (1 - p) * WMAX + ]).to_CNF() + AMDN._set_disorder_constraint_weights(constraint_1, disorder_constraints, (1 - p)) + # likewise, enforce the following constraint if the actions are disordered with weight p x wmax: - disorder_constraints[Or([ + constraint_2 = Or([ And([pre(r, act_y), ~delete(r, act_y), delete(r, act_x)]), And([add(r, act_y), pre(r, act_x)]), And([add(r, act_y), delete(r, act_x)]), And([delete(r, act_y), add(r, act_x)]) - ]).to_CNF()] = p * WMAX - # TODO: somehow convert to Or[Var] - # for c in disorder_constraints: - # if not c.is_CNF(): - # print("ERROR") - # print(And(c for c in disorder_constraints).to_CNF().is_CNF()) + ]).to_CNF() + AMDN._set_disorder_constraint_weights(constraint_2, disorder_constraints, p) return disorder_constraints - def _build_hard_parallel_constraints(self, obs_lists: ObservationLists): + @staticmethod + def _build_hard_parallel_constraints(obs_lists: ObservationLists): hard_constraints = {} # create a list of all tuples - for act in self.actions: + for act in obs_lists.actions: for r in obs_lists.probabilities: # for each action x proposition pair, enforce the two hard constraints with weight wmax hard_constraints[implies(add(r, act), ~pre(r, act))] = WMAX hard_constraints[implies(delete(r, act), pre(r, act))] = WMAX return hard_constraints - def _build_soft_parallel_constraints(self, obs_lists: ObservationLists): + @staticmethod + def _build_soft_parallel_constraints(obs_lists: ObservationLists): soft_constraints = {} # iterate through all traces for i in range(len(obs_lists.all_par_act_sets)): @@ -107,7 +129,7 @@ def _build_soft_parallel_constraints(self, obs_lists: ObservationLists): if act_x != act_x_prime: p = obs_lists.probabilities[ActionPair({act_x, act_x_prime})] # iterate through all propositions - for r in self.propositions: + for r in obs_lists.propositions: # equivalent: if r is in the add or delete list of an action in the set, that implies it # can't be in the add or delete list of any other action in the set soft_constraints[implies(Or([add(r, act_x_prime), delete(r, act_x_prime)]), Or([add(r, act_x), delete(r, act_x)]).negate())] = (1 - p) * WMAX @@ -123,14 +145,16 @@ def _build_soft_parallel_constraints(self, obs_lists: ObservationLists): if act_y != act_x_prime: p = obs_lists.probabilities[ActionPair({act_y, act_x_prime})] # iterate through all propositions and similarly set the constraint - for r in self.propositions: + for r in obs_lists.propositions: soft_constraints[implies(Or([add(r, act_x_prime), delete(r, act_x_prime)]), Or([add(r, act_y), delete(r, act_y)]).negate())] = p * WMAX return soft_constraints - def _build_parallel_constraints(self, obs_lists: ObservationLists): - return {**self._build_hard_parallel_constraints(obs_lists), **self._build_soft_parallel_constraints(obs_lists)} + @staticmethod + def _build_parallel_constraints(obs_lists: ObservationLists): + return {**AMDN._build_hard_parallel_constraints(obs_lists), **AMDN._build_soft_parallel_constraints(obs_lists)} - def _calculate_all_r_occ(self, obs_lists: ObservationLists): + @staticmethod + def _calculate_all_r_occ(obs_lists: ObservationLists): # tracks occurrences of all propositions all_occ = 0 for trace in obs_lists: @@ -138,18 +162,20 @@ def _calculate_all_r_occ(self, obs_lists: ObservationLists): all_occ += len([f for f in step.state if step.state[f]]) return all_occ - def _set_up_occurrences_dict(self): + @staticmethod + def _set_up_occurrences_dict(obs_lists: ObservationLists): # set up dict occurrences = {} - for a in self.actions: + for a in obs_lists.actions: occurrences[a] = {} - for r in self.propositions: + for r in obs_lists.propositions: occurrences[a][r] = 0 return occurrences - def _noise_constraints_6(self, obs_lists: ObservationLists, all_occ: int): + @staticmethod + def _noise_constraints_6(obs_lists: ObservationLists, all_occ: int, occ_threshold: int): noise_constraints_6 = {} - occurrences = self._set_up_occurrences_dict() + occurrences = AMDN._set_up_occurrences_dict(obs_lists) # iterate over ALL the plan traces, adding occurrences accordingly for i in range(len(obs_lists)): @@ -166,16 +192,17 @@ def _noise_constraints_6(self, obs_lists: ObservationLists, all_occ: int): for r in occurrences[a]: occ_r = occurrences[a][r] # if the # of occurrences is higher than the user-provided threshold: - if occ_r > self.occ_threshold: + if occ_r > occ_threshold: # set constraint 6 with the calculated weight noise_constraints_6[~delete(r, a)] = (occ_r / all_occ) return noise_constraints_6 - def _noise_constraints_7(self, obs_lists: ObservationLists, all_occ: int): + @staticmethod + def _noise_constraints_7(obs_lists: ObservationLists, all_occ: int): noise_constraints_7 = {} # set up dict occurrences = {} - for r in self.propositions: + for r in obs_lists.propositions: occurrences[r] = 0 for trace in obs_lists.states: @@ -199,9 +226,10 @@ def _noise_constraints_7(self, obs_lists: ObservationLists, all_occ: int): return noise_constraints_7 - def _noise_constraints_8(self, obs_lists, all_occ: int): + @staticmethod + def _noise_constraints_8(obs_lists, all_occ: int, occ_threshold: int): noise_constraints_8 = {} - occurrences = self._set_up_occurrences_dict() + occurrences = AMDN._set_up_occurrences_dict() # iterate over ALL the plan traces, adding occurrences accordingly for i in range(len(obs_lists)): @@ -218,34 +246,39 @@ def _noise_constraints_8(self, obs_lists, all_occ: int): for r in occurrences[a]: occ_r = occurrences[a][r] # if the # of occurrences is higher than the user-provided threshold: - if occ_r > self.occ_threshold: + if occ_r > occ_threshold: # set constraint 8 with the calculated weight noise_constraints_8[pre(r, a)] = (occ_r / all_occ) return noise_constraints_8 - def _build_noise_constraints(self, obs_lists: ObservationLists): + @staticmethod + def _build_noise_constraints(obs_lists: ObservationLists, occ_threshold: int): # calculate all occurrences for use in weights - all_occ = self._calculate_all_r_occ(obs_lists) - return{**self._noise_constraints_6(obs_lists, all_occ), **self._noise_constraints_7(obs_lists, all_occ), **self._noise_constraints_8(obs_lists, all_occ)} + all_occ = AMDN._calculate_all_r_occ(obs_lists) + return{**AMDN._noise_constraints_6(obs_lists, all_occ, occ_threshold), **AMDN._noise_constraints_7(obs_lists, all_occ), **AMDN._noise_constraints_8(obs_lists, all_occ, occ_threshold)} - def _set_all_constraints(self, obs_lists: ObservationLists): - return self._build_disorder_constraints(obs_lists) - #return {**self._build_disorder_constraints(obs_lists), ** self._build_parallel_constraints(obs_lists), **self._build_noise_constraints(obs_lists)} + @staticmethod + def _set_all_constraints(obs_lists: ObservationLists, occ_threshold: int): + return {**AMDN._build_disorder_constraints(obs_lists), ** AMDN._build_parallel_constraints(obs_lists), **AMDN._build_noise_constraints(obs_lists, occ_threshold)} - def _solve_constraints(self, obs_lists: ObservationLists): - constraints = self._set_all_constraints(obs_lists) + @staticmethod + def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int): + constraints = AMDN._set_all_constraints(obs_lists, occ_threshold) problem = [] + weights = [] constraints_ls : And[Or[Var]] = list(constraints.keys()) for c in constraints_ls: for f in c.children: problem.append(f) + weights.append(constraints[c]) problem = And(problem) print(problem.is_CNF()) - weights = list(constraints.values()) + # weights = list(constraints.values()) wcnf, decode = to_wcnf(problem, weights) return wcnf, decode - def _convert_to_model(self): + @staticmethod + def _convert_to_model(): # TODO: # convert the result to a Model pass \ No newline at end of file diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index b6cf6dcf..0d0b6e40 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -33,6 +33,8 @@ def __init__(self, traces: TraceList, Token: Type[Observation] = NoisyPartialDis actions = {step.action for trace in traces for step in trace if step.action} # cast to list for iteration purposes self.actions = list(actions) + # set of all fluents + self.propositions = {f for trace in self for step in trace for f in step.state.fluents} # create |A| (action x action set, no duplicates) self.cross_actions = [ActionPair({self.actions[i], self.actions[j]}) for i in range(len(self.actions)) for j in range(i, len(self.actions)) if i != j] diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 7c7378e0..335c451e 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -32,6 +32,7 @@ def test_tokenization_error(): enforced_hill_climbing_sampling=True ).traces + observations = traces.tokenize( Token=NoisyPartialDisorderedParallelObservation, ObsLists=DisorderedParallelActionsObservationLists, From 0ea58c04b14370e284cf93ef7851510f620777dd Mon Sep 17 00:00:00 2001 From: beckydvn Date: Mon, 16 Aug 2021 17:28:49 -0400 Subject: [PATCH 098/181] fix constraint weights --- macq/extract/amdn.py | 54 +++++++++---------- ...ered_parallel_actions_observation_lists.py | 2 +- tests/extract/test_amdn.py | 6 +-- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 4c3f77f7..5e5f5420 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -52,10 +52,10 @@ def _or_refactor(maybe_lit: Union[Or, Var]): Returns: A corresponding fluent of type "Or." """ - return Or([maybe_lit]) if isinstance(maybe_lit, Var) or isinstance(maybe_lit, Aux) else maybe_lit + return Or([maybe_lit]) if isinstance(maybe_lit, Var) else maybe_lit @staticmethod - def _set_disorder_constraint_weights(cnf_formula: And[Or[Var]], disorder_constraints: Dict, prob_disordered: float): + def _extract_aux_set_weights(cnf_formula: And[Or[Var]], constraints: Dict, prob_disordered: float): aux_var = set() # find all the auxiliary variables for clause in cnf_formula.children: @@ -63,10 +63,10 @@ def _set_disorder_constraint_weights(cnf_formula: And[Or[Var]], disorder_constra if isinstance(var.name, Aux): aux_var.add(var.name) # set each original constraint to be a hard clause - disorder_constraints[clause] = "HARD" + constraints[clause] = "HARD" # aux variables are the soft clauses that get the original weight for aux in aux_var: - disorder_constraints[AMDN._or_refactor(aux)] = prob_disordered * WMAX + constraints[AMDN._or_refactor(Var(aux))] = prob_disordered * WMAX @staticmethod def _build_disorder_constraints(obs_lists: ObservationLists): @@ -85,22 +85,20 @@ def _build_disorder_constraints(obs_lists: ObservationLists): # for each action combination, iterate through all possible propositions for r in obs_lists.propositions: # enforce the following constraint if the actions are ordered with weight (1 - p) x wmax: - constraint_1 = Or([ + AMDN._extract_aux_set_weights(Or([ And([pre(r, act_x), ~delete(r, act_x), delete(r, act_y)]), And([add(r, act_x), pre(r, act_y)]), And([add(r, act_x), delete(r, act_y)]), And([delete(r, act_x), add(r, act_y)]) - ]).to_CNF() - AMDN._set_disorder_constraint_weights(constraint_1, disorder_constraints, (1 - p)) + ]).to_CNF(), disorder_constraints, (1 - p)) # likewise, enforce the following constraint if the actions are disordered with weight p x wmax: - constraint_2 = Or([ + AMDN._extract_aux_set_weights(Or([ And([pre(r, act_y), ~delete(r, act_y), delete(r, act_x)]), And([add(r, act_y), pre(r, act_x)]), And([add(r, act_y), delete(r, act_x)]), And([delete(r, act_y), add(r, act_x)]) - ]).to_CNF() - AMDN._set_disorder_constraint_weights(constraint_2, disorder_constraints, p) + ]).to_CNF(), disorder_constraints, p) return disorder_constraints @staticmethod @@ -132,7 +130,7 @@ def _build_soft_parallel_constraints(obs_lists: ObservationLists): for r in obs_lists.propositions: # equivalent: if r is in the add or delete list of an action in the set, that implies it # can't be in the add or delete list of any other action in the set - soft_constraints[implies(Or([add(r, act_x_prime), delete(r, act_x_prime)]), Or([add(r, act_x), delete(r, act_x)]).negate())] = (1 - p) * WMAX + AMDN._extract_aux_set_weights(Or([And([~add(r, act_x_prime), ~delete(r, act_x_prime)]), And([~add(r, act_x), ~delete(r, act_x)])]).to_CNF(), soft_constraints, (1 - p)) # iterate through all traces for i in range(len(obs_lists.all_par_act_sets)): @@ -146,7 +144,7 @@ def _build_soft_parallel_constraints(obs_lists: ObservationLists): p = obs_lists.probabilities[ActionPair({act_y, act_x_prime})] # iterate through all propositions and similarly set the constraint for r in obs_lists.propositions: - soft_constraints[implies(Or([add(r, act_x_prime), delete(r, act_x_prime)]), Or([add(r, act_y), delete(r, act_y)]).negate())] = p * WMAX + AMDN._extract_aux_set_weights(Or([And([~add(r, act_x_prime), ~delete(r, act_x_prime)]), And([~add(r, act_y), ~delete(r, act_y)])]).to_CNF(), soft_constraints, p) return soft_constraints @staticmethod @@ -194,7 +192,7 @@ def _noise_constraints_6(obs_lists: ObservationLists, all_occ: int, occ_threshol # if the # of occurrences is higher than the user-provided threshold: if occ_r > occ_threshold: # set constraint 6 with the calculated weight - noise_constraints_6[~delete(r, a)] = (occ_r / all_occ) + noise_constraints_6[AMDN._or_refactor(~delete(r, a))] = (occ_r / all_occ) return noise_constraints_6 @staticmethod @@ -229,7 +227,7 @@ def _noise_constraints_7(obs_lists: ObservationLists, all_occ: int): @staticmethod def _noise_constraints_8(obs_lists, all_occ: int, occ_threshold: int): noise_constraints_8 = {} - occurrences = AMDN._set_up_occurrences_dict() + occurrences = AMDN._set_up_occurrences_dict(obs_lists) # iterate over ALL the plan traces, adding occurrences accordingly for i in range(len(obs_lists)): @@ -248,7 +246,7 @@ def _noise_constraints_8(obs_lists, all_occ: int, occ_threshold: int): # if the # of occurrences is higher than the user-provided threshold: if occ_r > occ_threshold: # set constraint 8 with the calculated weight - noise_constraints_8[pre(r, a)] = (occ_r / all_occ) + noise_constraints_8[AMDN._or_refactor(pre(r, a))] = (occ_r / all_occ) return noise_constraints_8 @staticmethod @@ -259,22 +257,24 @@ def _build_noise_constraints(obs_lists: ObservationLists, occ_threshold: int): @staticmethod def _set_all_constraints(obs_lists: ObservationLists, occ_threshold: int): - return {**AMDN._build_disorder_constraints(obs_lists), ** AMDN._build_parallel_constraints(obs_lists), **AMDN._build_noise_constraints(obs_lists, occ_threshold)} + return {**AMDN._build_disorder_constraints(obs_lists), **AMDN._build_parallel_constraints(obs_lists), **AMDN._build_noise_constraints(obs_lists, occ_threshold)} @staticmethod def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int): constraints = AMDN._set_all_constraints(obs_lists, occ_threshold) - problem = [] - weights = [] - constraints_ls : And[Or[Var]] = list(constraints.keys()) - for c in constraints_ls: - for f in c.children: - problem.append(f) - weights.append(constraints[c]) - problem = And(problem) - print(problem.is_CNF()) - # weights = list(constraints.values()) - wcnf, decode = to_wcnf(problem, weights) + # extract hard constraints + hard_constraints = [] + for c, weight in constraints.items(): + if weight == "HARD": + hard_constraints.append(c) + + for c in hard_constraints: + del constraints[c] + + wcnf, decode = to_wcnf(And(constraints.keys()), list(constraints.values())) + hard_wcnf, decode = to_wcnf(And(hard_constraints), None) + wcnf.extend(hard_wcnf.soft) + return wcnf, decode @staticmethod diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index 0d0b6e40..1a82379c 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -34,7 +34,7 @@ def __init__(self, traces: TraceList, Token: Type[Observation] = NoisyPartialDis # cast to list for iteration purposes self.actions = list(actions) # set of all fluents - self.propositions = {f for trace in self for step in trace for f in step.state.fluents} + self.propositions = {f for trace in traces for step in trace for f in step.state.fluents} # create |A| (action x action set, no duplicates) self.cross_actions = [ActionPair({self.actions[i], self.actions[j]}) for i in range(len(self.actions)) for j in range(i, len(self.actions)) if i != j] diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 335c451e..89af3e15 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -22,9 +22,9 @@ def test_tokenization_error(): # TODO: replace with a domain-specific random trace generator traces = RandomGoalSampling( - #problem_id=2337, - dom=dom, - prob=prob, + problem_id=2337, + # dom=dom, + # prob=prob, observe_pres_effs=True, num_traces=3, steps_deep=10, From dab584128fa1cdeeacf70f03cdc88071c2d5df2a Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 17 Aug 2021 13:41:25 -0400 Subject: [PATCH 099/181] convert to model --- macq/extract/__init__.py | 3 +- macq/extract/amdn.py | 80 +++++++++++++++++++++++++++++++------- macq/extract/exceptions.py | 26 +++++++++++++ macq/extract/extract.py | 7 ---- macq/extract/slaf.py | 4 +- tests/extract/test_amdn.py | 10 ++--- 6 files changed, 102 insertions(+), 28 deletions(-) create mode 100644 macq/extract/exceptions.py diff --git a/macq/extract/__init__.py b/macq/extract/__init__.py index 20142941..f4464129 100644 --- a/macq/extract/__init__.py +++ b/macq/extract/__init__.py @@ -1,7 +1,8 @@ from .model import Model -from .extract import Extract, modes, IncompatibleObservationToken, SLAF +from .extract import Extract, modes, SLAF from .learned_fluent import LearnedFluent from .learned_action import LearnedAction +from .exceptions import IncompatibleObservationToken __all__ = [ "Model", diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 5e5f5420..3122b5ed 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -1,7 +1,14 @@ +from macq.extract.learned_action import LearnedAction from nnf.operators import implies import macq.extract as extract -from typing import Dict, Union, Set +from typing import Dict, Union, Set, Hashable from nnf import Aux, Var, And, Or +from pysat.formula import WCNF +from pysat.examples.rc2 import RC2 +from .exceptions import ( + IncompatibleObservationToken, + InvalidMaxSATModel, +) from .model import Model from ..trace import ObservationLists, ActionPair, Fluent # for typing from ..observation import NoisyPartialDisorderedParallelObservation @@ -10,18 +17,18 @@ def __set_precond(r, act): return Var(str(r)[1:-1] + " is a precondition of " + act.details()) -def __set_del(r, act): - return Var(str(r)[1:-1] + " is deleted by " + act.details()) - def __set_add(r, act): return Var(str(r)[1:-1] + " is added by " + act.details()) +def __set_del(r, act): + return Var(str(r)[1:-1] + " is deleted by " + act.details()) + # for easier reference pre = __set_precond add = __set_add delete = __set_del -WMAX = 1 +WMAX = 10 class AMDN: def __new__(cls, obs_lists: ObservationLists, occ_threshold: int): @@ -35,11 +42,15 @@ def __new__(cls, obs_lists: ObservationLists, occ_threshold: int): Raised if the observations are not identity observation. """ if obs_lists.type is not NoisyPartialDisorderedParallelObservation: - raise extract.IncompatibleObservationToken(obs_lists.type, AMDN) + raise IncompatibleObservationToken(obs_lists.type, AMDN) - solve = AMDN._solve_constraints(obs_lists, occ_threshold) - print() - #return Model(fluents, actions) + return AMDN._amdn(obs_lists, occ_threshold) + + @staticmethod + def _amdn(obs_lists: ObservationLists, occ_threshold: int): + wcnf, decode = AMDN._solve_constraints(obs_lists, occ_threshold) + raw_model = AMDN._extract_raw_model(wcnf, decode) + return AMDN._extract_model(obs_lists, raw_model) @staticmethod def _or_refactor(maybe_lit: Union[Or, Var]): @@ -273,12 +284,55 @@ def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int): wcnf, decode = to_wcnf(And(constraints.keys()), list(constraints.values())) hard_wcnf, decode = to_wcnf(And(hard_constraints), None) - wcnf.extend(hard_wcnf.soft) + wcnf.extend(hard_wcnf) return wcnf, decode + # TODO: move out to utils @staticmethod - def _convert_to_model(): - # TODO: + def _extract_raw_model(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: + solver = RC2(max_sat) + encoded_model = solver.compute() + + if not isinstance(encoded_model, list): + # should never be reached + raise InvalidMaxSATModel(encoded_model) + + # decode the model (back to nnf vars) + model: Dict[Hashable, bool] = { + decode[abs(clause)]: clause > 0 for clause in encoded_model + } + return model + + @staticmethod + def _split_raw_fluent(raw_f: Hashable, learned_actions: Dict[str, LearnedAction]): + raw_f = str(raw_f) + pre_str = " is a precondition of " + add_str = " is added by " + del_str = " is deleted by " + if pre_str in raw_f: + f, act = raw_f.split(pre_str) + learned_actions[act].update_precond({f}) + elif add_str in raw_f: + f, act = raw_f.split(add_str) + learned_actions[act].update_add({f}) + else: + f, act = raw_f.split(del_str) + learned_actions[act].update_delete({f}) + + @staticmethod + def _extract_model(obs_lists: ObservationLists, model: Dict[Hashable, bool]): # convert the result to a Model - pass \ No newline at end of file + fluents = obs_lists.propositions + # set up LearnedActions + learned_actions = {} + for a in obs_lists.actions: + # set up a base LearnedAction with the known information + learned_actions[a.details()] = extract.LearnedAction(a.name, a.obj_params, cost=a.cost) + # iterate through all fluents + for raw_f in model: + # update learned_actions (ignore auxiliary variables) + if not isinstance(raw_f, Aux): + AMDN._split_raw_fluent(raw_f, learned_actions) + + return Model(fluents, learned_actions.values()) \ No newline at end of file diff --git a/macq/extract/exceptions.py b/macq/extract/exceptions.py new file mode 100644 index 00000000..7407cb23 --- /dev/null +++ b/macq/extract/exceptions.py @@ -0,0 +1,26 @@ +class IncompatibleObservationToken(Exception): + def __init__(self, token, technique, message=None): + if message is None: + message = f"Observations of type {token.__name__} are not compatible with the {technique.__name__} extraction technique." + super().__init__(message) + + +class InconsistentConstraintWeights(Exception): + def __init__(self, constraint, weight1, weight2, message=None): + if message is None: + message = f"Tried to assign the constraint {constraint} conflicting weights ({weight1} and {weight2})." + super().__init__(message) + + +class InvalidMaxSATModel(Exception): + def __init__(self, model, message=None): + if message is None: + message = f"The MAX-SAT solver generated an invalid model. Model should be a list of integers. model = {model}." + super().__init__(message) + + +class ConstraintContradiction(Exception): + def __init__(self, relation, effect, action, message=None): + if message is None: + message = f"Action model has contradictory constraints for {relation}'s presence in the {effect} list of {action.details()}." + super().__init__(message) diff --git a/macq/extract/extract.py b/macq/extract/extract.py index 6e643abb..7b986af2 100644 --- a/macq/extract/extract.py +++ b/macq/extract/extract.py @@ -14,13 +14,6 @@ class SAS: post_state: State -class IncompatibleObservationToken(Exception): - def __init__(self, token, technique, message=None): - if message is None: - message = f"Observations of type {token.__name__} are not compatible with the {technique.__name__} extraction technique." - super().__init__(message) - - class modes(Enum): """Model extraction techniques. diff --git a/macq/extract/slaf.py b/macq/extract/slaf.py index ada95794..0bf82a34 100644 --- a/macq/extract/slaf.py +++ b/macq/extract/slaf.py @@ -2,6 +2,7 @@ from typing import Set, Union from nnf import Var, Or, And, true, false, config from bauhaus import Encoding +from .exceptions import IncompatibleObservationToken from .model import Model from .learned_fluent import LearnedFluent from ..observation import AtomicPartialObservation @@ -52,7 +53,7 @@ def __new__(cls, o_list: ObservationLists, debug_mode: bool = False): Raised if the observations are not identity observation. """ if o_list.type is not AtomicPartialObservation: - raise extract.IncompatibleObservationToken(o_list.type, SLAF) + raise IncompatibleObservationToken(o_list.type, SLAF) SLAF.debug_mode = debug_mode entailed = SLAF.__as_strips_slaf(o_list) # return the Model @@ -162,7 +163,6 @@ def __sort_results(observations: ObservationLists, entailed: Set): The extracted `Model`. """ learned_actions = {} - base_fluents = {} model_fluents = set() # iterate through each step for o in observations: diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 89af3e15..87652504 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -22,12 +22,12 @@ def test_tokenization_error(): # TODO: replace with a domain-specific random trace generator traces = RandomGoalSampling( - problem_id=2337, - # dom=dom, - # prob=prob, + # problem_id=2337, + dom=dom, + prob=prob, observe_pres_effs=True, num_traces=3, - steps_deep=10, + steps_deep=30, subset_size_perc=0.1, enforced_hill_climbing_sampling=True ).traces @@ -41,4 +41,4 @@ def test_tokenization_error(): ) print() model = Extract(observations, modes.AMDN, occ_threshold=3) - #print(model.details()) \ No newline at end of file + print(model.details()) \ No newline at end of file From 372085eb9bf46b9a741ddc9b3cb1ff46d6ceadbf Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 17 Aug 2021 17:00:34 -0400 Subject: [PATCH 100/181] split up functions in pysat, get 1 proper wcnf --- macq/extract/amdn.py | 10 ++++------ macq/utils/pysat.py | 23 ++++++++++++----------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 3122b5ed..ede6f5a4 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -12,7 +12,7 @@ from .model import Model from ..trace import ObservationLists, ActionPair, Fluent # for typing from ..observation import NoisyPartialDisorderedParallelObservation -from ..utils.pysat import to_wcnf +from ..utils.pysat import to_wcnf, encode def __set_precond(r, act): return Var(str(r)[1:-1] + " is a precondition of " + act.details()) @@ -28,7 +28,7 @@ def __set_del(r, act): add = __set_add delete = __set_del -WMAX = 10 +WMAX = 1 class AMDN: def __new__(cls, obs_lists: ObservationLists, occ_threshold: int): @@ -117,7 +117,7 @@ def _build_hard_parallel_constraints(obs_lists: ObservationLists): hard_constraints = {} # create a list of all tuples for act in obs_lists.actions: - for r in obs_lists.probabilities: + for r in obs_lists.propositions: # for each action x proposition pair, enforce the two hard constraints with weight wmax hard_constraints[implies(add(r, act), ~pre(r, act))] = WMAX hard_constraints[implies(delete(r, act), pre(r, act))] = WMAX @@ -282,9 +282,7 @@ def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int): for c in hard_constraints: del constraints[c] - wcnf, decode = to_wcnf(And(constraints.keys()), list(constraints.values())) - hard_wcnf, decode = to_wcnf(And(hard_constraints), None) - wcnf.extend(hard_wcnf) + wcnf, decode = to_wcnf(soft_clauses=And(constraints.keys()), hard_clauses=And(hard_constraints), weights=list(constraints.values())) return wcnf, decode diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py index 44201126..e3a3d647 100644 --- a/macq/utils/pysat.py +++ b/macq/utils/pysat.py @@ -8,25 +8,26 @@ def __init__(self, clauses): self.clauses = clauses super().__init__(f"Cannot convert a non CNF formula to WCNF") - -def _encode(clauses: And[Or[Var]]) -> Tuple[List[List[int]], Dict[int, Hashable]]: - decode = dict(enumerate(clauses.vars(), start=1)) +def encode_dict(clauses: And[Or[Var]], start: int = 1):# -> Tuple[List[List[int]], Dict[int, Hashable]]: + decode = dict(enumerate(clauses.vars(), start=start)) encode = {v: k for k, v in decode.items()} + return decode, encode + +def encode(clauses: And[Or[Var]], encode):# -> Tuple[List[List[int]], Dict[int, Hashable]]: encoded = [ [encode[var.name] if var.true else -encode[var.name] for var in clause] for clause in clauses ] - - return encoded, decode - + return encoded def to_wcnf( - clauses: And[Or[Var]], weights: List[int] + soft_clauses: And[Or[Var]], hard_clauses: And[Or[Var]], weights: List[int] ) -> Tuple[WCNF, Dict[int, Hashable]]: """Converts a python-nnf CNF formula to a pysat WCNF.""" - # if not clauses.is_CNF(): - # raise NotCNF(clauses) - encoded, decode = _encode(clauses) + soft_decode, soft_encode = encode_dict(soft_clauses) + hard_decode, hard_encode = encode_dict(hard_clauses, start = len(soft_decode) + 1) wcnf = WCNF() - wcnf.extend(encoded, weights) + wcnf.extend(encode(soft_clauses, soft_encode), weights) + wcnf.extend(encode(hard_clauses, hard_encode)) + decode = {**soft_decode, **hard_decode} return wcnf, decode \ No newline at end of file From 2f4b9b1faa02a884c52babfca97438e929f097ab Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 18 Aug 2021 16:26:24 -0400 Subject: [PATCH 101/181] Make AtomicPartialObservation work like PartialObservation, add atomic Action and States --- .../observation/atomic_partial_observation.py | 138 ++++-------------- macq/observation/partial_observation.py | 11 +- macq/trace/action.py | 27 ++-- macq/trace/state.py | 16 +- test_model.json | 1 - 5 files changed, 59 insertions(+), 134 deletions(-) delete mode 100644 test_model.json diff --git a/macq/observation/atomic_partial_observation.py b/macq/observation/atomic_partial_observation.py index eb72b925..6a48f105 100644 --- a/macq/observation/atomic_partial_observation.py +++ b/macq/observation/atomic_partial_observation.py @@ -1,9 +1,7 @@ +from logging import warning from ..trace import Step, Fluent -from ..trace import PartialState -from . import Observation, InvalidQueryParameter -from typing import Callable, Union, Set, List, Optional -from dataclasses import dataclass -import random +from . import PartialObservation +from typing import Set class PercentError(Exception): @@ -16,134 +14,48 @@ def __init__( super().__init__(message) -class AtomicPartialObservation(Observation): +class AtomicPartialObservation(PartialObservation): """The Atomic Partial Observability Token. - The atomic partial observability token stores the step where some of the values of the fluents in the step's state are unknown. Inherits the base Observation class. Unlike the partial observability token, the atomic partial observability token stores everything in strings. """ - # used these to store action and state info with just strings - class IdentityState(dict): - def __hash__(self): - return hash(tuple(sorted(self.items()))) - - @dataclass - class IdentityAction: - name: str - obj_params: List[str] - cost: Optional[int] - - def __str__(self): - objs_str = "" - for o in self.obj_params: - objs_str += o + " " - return " ".join([self.name, objs_str]) + "[" + str(self.cost) + "]" - - def __hash__(self): - return hash(str(self)) - def __init__( - self, - step: Step, - method: Union[Callable[[int], Step], Callable[[Set[Fluent]], Step]], - **method_kwargs, + self, step: Step, percent_missing: float = 0, hide: Set[Fluent] = None ): """ - Creates an PartialObservation object, storing the step. + Creates an AtomicPartialObservation object, storing the step. Args: step (Step): The step associated with this observation. - method (function reference): - The method to be used to tokenize the step. - **method_kwargs (keyword arguments): - The arguments to be passed to the corresponding method function. - """ - super().__init__(index=step.index) - step = method(self, step, **method_kwargs) - self.state = self.IdentityState( - {str(fluent): value for fluent, value in step.state.items()} - ) - self.action = ( - None - if step.action is None - else self.IdentityAction( - step.action.name, - list(map(lambda o: o.details(), step.action.obj_params)), - step.action.cost, - ) - ) - - def __eq__(self, other): - if not isinstance(other, AtomicPartialObservation): - return False - return self.state == other.state and self.action == other.action - - # and here is the old matches function - - def _matches(self, key: str, value: str): - if key == "action": - if self.action is None: - return value is None - return str(self.action) == value - elif key == "fluent_holds": - return self.state[value] - else: - raise InvalidQueryParameter(AtomicPartialObservation, key) - - def details(self): - return f"Obs {str(self.index)}.\n State: {str(self.state)}\n Action: {str(self.action)}" - - def random_subset(self, step: Step, percent_missing: float): - """Method of tokenization that picks a random subset of fluents to hide. - - Args: - step (Step): - The step to tokenize. percent_missing (float): - The percentage of fluents to hide. - - Returns: - The new step created using a PartialState that takes the hidden fluents into account. + The percentage of fluents to randomly hide in the observation. + hide (Set[Fluent]): + The set of fluents to explicitly hide in the observation. """ if percent_missing > 1 or percent_missing < 0: raise PercentError() - fluents = step.state.fluents - num_new_fluents = int(len(fluents) * (percent_missing)) + if percent_missing == 0 and not hide: + warning("Creating a PartialObseration with no missing information.") - new_fluents = {} - # shuffle keys and take an appropriate subset of them - hide_fluents_ls = list(fluents) - random.shuffle(hide_fluents_ls) - hide_fluents_ls = hide_fluents_ls[:num_new_fluents] - # get new dict - for f in fluents: - if f in hide_fluents_ls: - new_fluents[f] = None - else: - new_fluents[f] = step.state[f] - return Step(PartialState(new_fluents), step.action, step.index) + self.index = step.index - def same_subset(self, step: Step, hide_fluents: Set[Fluent]): - """Method of tokenization that hides the same subset of fluents every time. + if percent_missing < 1: + step = self.random_subset(step, percent_missing) + if hide: + step = self.hide_subset(step, hide) - Args: - step (Step): - The step to tokenize. - hide_fluents (Set[Fluent]): - The set of fluents that will be hidden each time. + self.state = None if percent_missing == 1 else step.state.clone(atomic=True) + self.action = None if step.action is None else step.action.clone(atomic=True) - Returns: - The new step created using a PartialState that takes the hidden fluents into account. - """ - new_fluents = {} - for f in step.state.fluents: - if f in hide_fluents: - new_fluents[f] = None - else: - new_fluents[f] = step.state[f] - return Step(PartialState(new_fluents), step.action, step.index) + def __eq__(self, other): + if not isinstance(other, AtomicPartialObservation): + return False + return self.state == other.state and self.action == other.action + + def details(self): + return f"Obs {str(self.index)}.\n State: {str(self.state)}\n Action: {str(self.action)}" diff --git a/macq/observation/partial_observation.py b/macq/observation/partial_observation.py index a54e98a1..2302452a 100644 --- a/macq/observation/partial_observation.py +++ b/macq/observation/partial_observation.py @@ -19,7 +19,7 @@ def __init__( self, step: Step, percent_missing: float = 0, hide: Set[Fluent] = None ): """ - Creates an PartialObservation object, storing the step. + Creates a PartialObservation object, storing the step. Args: step (Step): @@ -50,11 +50,9 @@ def __init__( self.action = None if step.action is None else step.action.clone() def __eq__(self, other): - return ( - isinstance(other, PartialObservation) - and self.state == other.state - and self.action == other.action - ) + if not isinstance(other, PartialObservation): + return False + return self.state == other.state and self.action == other.action def random_subset(self, step: Step, percent_missing: float): """Hides a random subset of the fluents in the step. @@ -68,7 +66,6 @@ def random_subset(self, step: Step, percent_missing: float): Returns: A Step whose state is a PartialState with the random fluents hidden. """ - fluents = step.state.fluents num_new_fluents = int(len(fluents) * (percent_missing)) diff --git a/macq/trace/action.py b/macq/trace/action.py index 6df95fab..185c3d8c 100644 --- a/macq/trace/action.py +++ b/macq/trace/action.py @@ -1,5 +1,5 @@ -from typing import List, Set -from .fluent import Fluent, PlanningObject +from typing import List +from .fluent import PlanningObject class Action: @@ -18,12 +18,7 @@ class Action: The cost to perform the action. """ - def __init__( - self, - name: str, - obj_params: List[PlanningObject], - cost: int = 0, - ): + def __init__(self, name: str, obj_params: List[PlanningObject], cost: int = 0): """Initializes an Action with the parameters provided. The `precond`, `add`, and `delete` args should only be provided in Model deserialization. @@ -59,7 +54,12 @@ def details(self): string = f"{self.name} {' '.join([o.details() for o in self.obj_params])}" return string - def clone(self): + def clone(self, atomic=False): + if atomic: + return AtomicAction( + self.name, list(map(lambda o: o.details(), self.obj_params)), self.cost + ) + return Action(self.name, self.obj_params, self.cost) def add_parameter(self, obj: PlanningObject): @@ -73,3 +73,12 @@ def add_parameter(self, obj: PlanningObject): def _serialize(self): return self.name + + +class AtomicAction(Action): + """An Action where the objects are represented by strings.""" + + def __init__(self, name: str, obj_params: List[str], cost: int = 0): + self.name = name + self.obj_params = obj_params + self.cost = cost diff --git a/macq/trace/state.py b/macq/trace/state.py index 06afab39..ec9ceb71 100644 --- a/macq/trace/state.py +++ b/macq/trace/state.py @@ -1,5 +1,4 @@ from __future__ import annotations -from dataclasses import dataclass from typing import Dict from rich.text import Text from . import Fluent @@ -16,7 +15,7 @@ class State: A mapping of `Fluent` objects to their value in this state. """ - def __init__(self, fluents: Dict[Fluent, bool] = {}): + def __init__(self, fluents: Dict[Fluent, bool] = None): """Initializes State with an optional fluent-value mapping. Args: @@ -24,7 +23,7 @@ def __init__(self, fluents: Dict[Fluent, bool] = {}): Optional; A mapping of `Fluent` objects to their value in this state. Defaults to an empty `dict`. """ - self.fluents = fluents + self.fluents = fluents if fluents is not None else {} def __eq__(self, other): if not isinstance(other, State): @@ -84,10 +83,19 @@ def details(self): string.append(", ") return string[:-2] - def clone(self): + def clone(self, atomic=False): + if atomic: + return AtomicState({str(fluent): value for fluent, value in self.items()}) return State(self.fluents) def holds(self, fluent: str): fluents = dict(map(lambda f: (f.name, f), self.keys())) if fluent in fluents.keys(): return self[fluents[fluent]] + + +class AtomicState(State): + """A State where the fluents are represented by strings.""" + + def __init__(self, fluents: Dict[str, bool] = None): + self.fluents = fluents if fluents is not None else {} diff --git a/test_model.json b/test_model.json deleted file mode 100644 index 7bbdfb0f..00000000 --- a/test_model.json +++ /dev/null @@ -1 +0,0 @@ -{"fluents": ["(on object f object c)", "(ontable object i)", "(on object b object f)", "(on object e object h)", "(on object a object f)", "(on object d object c)", "(on object i object a)", "(on object f object g)", "(on object c object j)", "(on object f object i)", "(clear object d)", "(on object c object f)", "(on object a object i)", "(on object j object d)", "(ontable object h)", "(on object i object e)", "(holding object f)", "(clear object e)", "(on object j object a)", "(on object d object j)", "(on object f object h)", "(on object h object f)", "(on object i object d)", "(on object g object h)", "(on object g object g)", "(on object f object b)", "(ontable object j)", "(clear object j)", "(on object d object h)", "(on object h object d)", "(on object i object i)", "(on object c object d)", "(on object h object i)", "(on object b object j)", "(on object j object c)", "(on object e object c)", "(on object c object c)", "(on object g object f)", "(on object i object f)", "(on object h object h)", "(on object j object i)", "(on object c object b)", "(holding object e)", "(on object d object a)", "(ontable object d)", "(on object a object a)", "(on object h object j)", "(holding object j)", "(on object b object g)", "(on object f object e)", "(on object j object f)", "(ontable object a)", "(on object i object g)", "(on object d object f)", "(on object j object g)", "(holding object d)", "(on object j object b)", "(on object g object e)", "(on object d object e)", "(on object d object b)", "(on object e object g)", "(on object e object a)", "(on object c object e)", "(on object d object g)", "(on object j object e)", "(ontable object e)", "(on object a object d)", "(on object a object g)", "(on object e object d)", "(clear object c)", "(on object g object d)", "(on object e object b)", "(on object b object e)", "(holding object i)", "(ontable object g)", "(clear object b)", "(on object e object i)", "(on object c object g)", "(on object j object h)", "(clear object h)", "(on object c object i)", "(on object d object d)", "(on object e object e)", "(on object a object b)", "(on object a object j)", "(on object e object f)", "(holding object h)", "(on object g object j)", "(on object b object c)", "(on object h object g)", "(on object b object d)", "(on object a object e)", "(on object f object f)", "(on object f object a)", "(on object g object b)", "(on object i object c)", "(on object i object h)", "(handempty )", "(clear object a)", "(ontable object f)", "(on object i object j)", "(on object c object a)", "(holding object g)", "(holding object b)", "(on object b object b)", "(on object g object c)", "(on object a object h)", "(on object g object a)", "(on object b object a)", "(on object h object b)", "(on object f object d)", "(ontable object b)", "(on object d object i)", "(on object h object a)", "(clear object g)", "(on object h object c)", "(clear object f)", "(ontable object c)", "(on object h object e)", "(clear object i)", "(on object c object h)", "(on object a object c)", "(on object j object j)", "(on object f object j)", "(on object e object j)", "(holding object a)", "(on object b object h)", "(on object b object i)", "(holding object c)", "(on object i object b)", "(on object g object i)"], "actions": [{"name": "put-down", "obj_params": ["object c"], "cost": 0, "precond": ["(on object d object i)", "(on object g object h)", "(on object h object a)", "(ontable object i)", "(on object e object j)", "(clear object f)", "(clear object e)", "(on object j object b)", "(on object b object g)", "(holding object c)", "(ontable object f)", "(on object a object d)"], "add": ["(ontable object c)", "(handempty )", "(clear object c)"], "delete": ["(holding object c)"]}, {"name": "pick-up", "obj_params": ["object c"], "cost": 0, "precond": ["(ontable object c)", "(on object g object h)", "(on object h object a)", "(on object d object i)", "(clear object c)", "(ontable object i)", "(on object e object j)", "(clear object f)", "(clear object e)", "(handempty )", "(on object j object b)", "(on object b object g)", "(ontable object f)", "(on object a object d)"], "add": ["(holding object c)"], "delete": ["(ontable object c)", "(handempty )", "(clear object c)"]}, {"name": "stack", "obj_params": ["object f", "object c"], "cost": 0, "precond": ["(on object d object i)", "(on object g object h)", "(on object h object a)", "(ontable object i)", "(on object e object j)", "(on object j object b)", "(on object b object g)", "(on object a object d)"], "add": ["(clear object f)", "(handempty )", "(on object f object c)", "(clear object c)", "(on object c object f)"], "delete": ["(clear object f)", "(holding object f)", "(clear object c)", "(holding object c)"]}, {"name": "unstack", "obj_params": ["object f", "object c"], "cost": 0, "precond": ["(on object d object i)", "(on object g object h)", "(on object h object a)", "(on object f object c)", "(ontable object i)", "(on object c object e)", "(on object e object j)", "(clear object f)", "(handempty )", "(on object j object b)", "(on object b object g)", "(on object a object d)"], "add": ["(clear object c)", "(holding object f)"], "delete": ["(clear object f)", "(handempty )", "(on object f object c)"]}, {"name": "stack", "obj_params": ["object e", "object c"], "cost": 0, "precond": ["(on object d object i)", "(on object g object h)", "(on object h object a)", "(ontable object i)", "(clear object f)", "(on object j object b)", "(on object b object g)", "(ontable object f)", "(on object a object d)"], "add": ["(clear object e)", "(handempty )", "(on object c object e)", "(clear object c)", "(on object e object c)"], "delete": ["(clear object e)", "(holding object e)", "(holding object c)", "(clear object c)"]}, {"name": "unstack", "obj_params": ["object e", "object j"], "cost": 0, "precond": ["(ontable object c)", "(on object g object h)", "(on object h object a)", "(on object d object i)", "(clear object c)", "(ontable object i)", "(on object e object j)", "(clear object f)", "(clear object e)", "(handempty )", "(on object j object b)", "(on object b object g)", "(ontable object f)", "(on object a object d)"], "add": ["(holding object e)", "(clear object j)"], "delete": ["(clear object e)", "(handempty )", "(on object e object j)"]}, {"name": "unstack", "obj_params": ["object e", "object c"], "cost": 0, "precond": ["(on object d object i)", "(on object g object h)", "(on object h object a)", "(clear object c)", "(ontable object i)", "(on object c object e)", "(on object e object j)", "(clear object f)", "(handempty )", "(on object j object b)", "(on object b object g)", "(ontable object f)", "(on object a object d)"], "add": ["(clear object e)", "(holding object c)"], "delete": ["(handempty )", "(on object c object e)", "(clear object c)"]}, {"name": "put-down", "obj_params": ["object f"], "cost": 0, "precond": ["(on object d object i)", "(on object g object h)", "(on object h object a)", "(holding object f)", "(ontable object i)", "(on object e object j)", "(on object b object g)", "(on object j object b)", "(clear object c)", "(on object a object d)"], "add": ["(clear object f)", "(handempty )", "(ontable object f)"], "delete": ["(holding object f)"]}, {"name": "pick-up", "obj_params": ["object f"], "cost": 0, "precond": ["(on object d object i)", "(on object g object h)", "(on object h object a)", "(ontable object i)", "(on object e object j)", "(clear object f)", "(on object b object g)", "(handempty )", "(on object j object b)", "(clear object c)", "(ontable object f)", "(on object a object d)"], "add": ["(holding object f)"], "delete": ["(clear object f)", "(handempty )", "(ontable object f)"]}]} \ No newline at end of file From 29b05d276024ce3ce62ac7d2e341d2c407f51c3c Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 18 Aug 2021 16:44:07 -0400 Subject: [PATCH 102/181] Fix tests --- tests/extract/test_slaf.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/extract/test_slaf.py b/tests/extract/test_slaf.py index 6ce11f2d..09083f0d 100644 --- a/tests/extract/test_slaf.py +++ b/tests/extract/test_slaf.py @@ -10,7 +10,6 @@ def test_slaf(): traces = generate_blocks_traces(plan_len=2, num_traces=1) observations = traces.tokenize( AtomicPartialObservation, - method=AtomicPartialObservation.random_subset, percent_missing=0.10, ) model = Extract(observations, modes.SLAF) @@ -23,8 +22,12 @@ def test_slaf(): if __name__ == "__main__": # exit out to the base macq folder so we can get to /tests base = Path(__file__).parent.parent - model_blocks_dom = str((base / "generated_testing_files/model_blocks_domain.pddl").resolve()) - model_blocks_prob = str((base / "generated_testing_files/model_blocks_problem.pddl").resolve()) + model_blocks_dom = str( + (base / "generated_testing_files/model_blocks_domain.pddl").resolve() + ) + model_blocks_prob = str( + (base / "generated_testing_files/model_blocks_problem.pddl").resolve() + ) traces = generate_blocks_traces(plan_len=2, num_traces=1) observations = traces.tokenize( @@ -36,4 +39,6 @@ def test_slaf(): model = Extract(observations, modes.SLAF, debug_mode=True) print(model.details()) - model.to_pddl("model_blocks_dom", "model_blocks_prob", model_blocks_dom, model_blocks_prob) + model.to_pddl( + "model_blocks_dom", "model_blocks_prob", model_blocks_dom, model_blocks_prob + ) From 2733bb613e1216ca8c4e3ba6bc6e277150557892 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 18 Aug 2021 17:41:06 -0400 Subject: [PATCH 103/181] Use list for LearnedAction obj_params --- macq/extract/learned_action.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/macq/extract/learned_action.py b/macq/extract/learned_action.py index 9d6348d1..5620dc2d 100644 --- a/macq/extract/learned_action.py +++ b/macq/extract/learned_action.py @@ -1,9 +1,9 @@ from __future__ import annotations -from typing import Set +from typing import List, Set class LearnedAction: - def __init__(self, name: str, obj_params: Set[str], **kwargs): + def __init__(self, name: str, obj_params: List[str], **kwargs): self.name = name self.obj_params = obj_params if "cost" in kwargs: @@ -25,7 +25,7 @@ def __hash__(self): def details(self): # obj_params can be either a list of strings or a list of PlanningObject depending on the token type and extraction method used to learn the action try: - string = f"({self.name} {' '.join([o for o in self.obj_params])})" + string = f"({self.name} {' '.join(self.obj_params)})" except TypeError: string = f"({self.name} {' '.join([o.details() for o in self.obj_params])})" From b9fcf4e11d1f240c716968ac965d25620bab4490 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 18 Aug 2021 17:41:44 -0400 Subject: [PATCH 104/181] Filter out fluents that don't match the action schemata in step 2I --- macq/extract/arms.py | 46 +++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index f707121b..f4af667a 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -23,6 +23,15 @@ class Relation: def var(self): return f"{self.name} {' '.join(list(self.types))}" + def matches(self, action: LearnedAction): + match = False + action_types = set(action.obj_params) + self_counts = Counter(self.types) + action_counts = Counter(action.obj_params) + return all([t in action_types for t in self.types]) and all( + [self_counts[t] <= action_counts[t] for t in self.types] + ) + def __hash__(self): return hash(self.var()) @@ -259,7 +268,7 @@ def _step1( # Create LearnedActions for each action, replacing instantiated # objects with the object type. - types = {obj.obj_type for obj in obs_action.obj_params} + types = [obj.obj_type for obj in obs_action.obj_params] learned_action = LearnedAction(obs_action.name, types) learned_actions.add(learned_action) action_map[obs_action] = learned_action @@ -268,7 +277,7 @@ def _step1( for a1 in learned_actions: connected_actions[a1] = {} for a2 in learned_actions.difference({a1}): # includes connecting with self - intersection = a1.obj_params.intersection(a2.obj_params) + intersection = set(a1.obj_params).intersection(set(a2.obj_params)) if intersection: connected_actions[a1][a2] = intersection if debug: @@ -296,8 +305,8 @@ def _step2( lambda f: ( f, Relation( - f.name, # the fluent name - [obj.obj_type for obj in f.objects], # the object types + f.name, + [obj.obj_type for obj in f.objects], ), ), fluents, @@ -353,7 +362,7 @@ def implication(a: Var, b: Var): for action in actions: for relation in relations: # A relation is relevant to an action if they share parameter types - if relation.types and set(relation.types).issubset(action.obj_params): + if relation.matches(action): if debug: print( f'relation ({relation.var()}) is relevant to action "{action.details()}"\n' @@ -427,12 +436,13 @@ def _step2I( f"\nStep {i} of observation list {obs_list_i} contains state information." ) for fluent, val in obs.state.items(): + relation = relations[fluent] # Information constraints only apply to true relations if val: if debug: print( f" Fluent {fluent} is true.\n" - f" ({relations[fluent].var()})∈ (" + f" ({relation.var()})∈ (" f"{' ∪ '.join([f'add_{{ {actions[obs_list[ik].action].details()} }}' for ik in range(0,n+1) if obs_list[ik].action in actions] )}" # type: ignore ")" ) @@ -440,15 +450,14 @@ def _step2I( # relation in the add list of an action <= n (i-1) i1: List[Var] = [] for obs_i in obs_list[: i - 1]: - # action will never be None if it's in actions, - # but the condition is needed to make linting happy if obs_i.action in actions and obs_i.action is not None: ai = actions[obs_i.action] - i1.append( - Var( - f"{relations[fluent].var()} (BREAK) in (BREAK) add (BREAK) {ai.details()}" + if relation.matches(ai): + i1.append( + Var( + f"{relation.var()} (BREAK) in (BREAK) add (BREAK) {ai.details()}" + ) ) - ) # I2 # relation not in del list of action n (i-1) @@ -456,7 +465,7 @@ def _step2I( a_n = obs_list[i - 1].action if a_n in actions and a_n is not None: i2 = Var( - f"{relations[fluent].var()} (BREAK) in (BREAK) del (BREAK) {actions[a_n].details()}" + f"{relation.var()} (BREAK) in (BREAK) del (BREAK) {actions[a_n].details()}" ).negate() if i1: @@ -470,24 +479,29 @@ def _step2I( i < len(obs_list) - 1 and obs.action in actions and obs.action is not None # for the linter + and relation.matches(actions[obs.action]) ): # corresponding constraint is related to the current action's precondition list support_counts[ Or( [ Var( - f"{relations[fluent].var()} (BREAK) in (BREAK) pre (BREAK) {actions[obs.action].details()}" + f"{relation.var()} (BREAK) in (BREAK) pre (BREAK) {actions[obs.action].details()}" ) ] ) ] += 1 - elif a_n in actions and a_n is not None: + elif ( + a_n in actions + and a_n is not None + and relation.matches(actions[a_n]) + ): # corresponding constraint is related to the previous action's add list support_counts[ Or( [ Var( - f"{relations[fluent].var()} (BREAK) in (BREAK) add (BREAK) {actions[a_n].details()}" + f"{relation.var()} (BREAK) in (BREAK) add (BREAK) {actions[a_n].details()}" ) ] ) From 244a9cab8bcf39e3aa2d03474bf8f38bc140ab44 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 18 Aug 2021 18:10:23 -0400 Subject: [PATCH 105/181] Update pysat module --- macq/utils/pysat.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py index 74be5df7..86ebaa4c 100644 --- a/macq/utils/pysat.py +++ b/macq/utils/pysat.py @@ -3,31 +3,35 @@ from nnf import And, Or, Var -class NotCNF(Exception): - def __init__(self, clauses): - self.clauses = clauses - super().__init__(f"Cannot convert a non CNF formula to WCNF") - - -def _encode(clauses: And[Or[Var]]) -> Tuple[List[List[int]], Dict[int, Hashable]]: - decode = dict(enumerate(clauses.vars(), start=1)) +def get_encoding( + clauses: And[Or[Var]], start: int = 1 +) -> Tuple[Dict[Hashable, int], Dict[int, Hashable]]: + decode = dict(enumerate(clauses.vars(), start=start)) encode = {v: k for k, v in decode.items()} + return encode, decode + +def encode(clauses: And[Or[Var]], encode: Dict[Hashable, int]) -> List[List[int]]: encoded = [ [encode[var.name] if var.true else -encode[var.name] for var in clause] for clause in clauses ] - - return encoded, decode + return encoded def to_wcnf( - clauses: And[Or[Var]], weights: List[int] + soft_clauses: And[Or[Var]], weights: List[int], hard_clauses: And[Or[Var]] = None ) -> Tuple[WCNF, Dict[int, Hashable]]: """Converts a python-nnf CNF formula to a pysat WCNF.""" - # if not clauses.is_CNF(): - # raise NotCNF(clauses) - encoded, decode = _encode(clauses) wcnf = WCNF() + soft_encode, decode = get_encoding(soft_clauses) + encoded = encode(soft_clauses, soft_encode) wcnf.extend(encoded, weights) + + if hard_clauses: + hard_encode, hard_decode = get_encoding(hard_clauses, start=len(decode) + 1) + decode.update(hard_decode) + encoded = encode(hard_clauses, hard_encode) + wcnf.extend(encoded) + return wcnf, decode From 7725e16d06ad7ec8bffb403e142de9fa0d83f393 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 19 Aug 2021 17:40:55 -0400 Subject: [PATCH 106/181] Add debug arg to Observer --- macq/extract/observer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macq/extract/observer.py b/macq/extract/observer.py index f48ad783..17846b2b 100644 --- a/macq/extract/observer.py +++ b/macq/extract/observer.py @@ -31,7 +31,7 @@ class Observer: fluents that went from True to False. """ - def __new__(cls, obs_lists: ObservationLists): + def __new__(cls, obs_lists: ObservationLists, debug: bool): """Creates a new Model object. Args: From 158093ae3d4f944afce63d4c999c1f428638172a Mon Sep 17 00:00:00 2001 From: beckydvn Date: Thu, 19 Aug 2021 17:49:36 -0400 Subject: [PATCH 107/181] optimize prob calculation --- ...ered_parallel_actions_observation_lists.py | 25 +++++++++++++------ tests/extract/test_amdn.py | 8 +++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index 138f28d9..c93e25d0 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -91,6 +91,8 @@ class DisorderedParallelActionsObservationLists(ObservationLists): Attributes: traces (List[List[Token]]): The trace list converted to a list of lists of tokens. + type (Type[Observation]): + The type of token to be used. features (List[Callable]): The list of functions to be used to create the feature vector. learned_theta (List[float]): @@ -99,6 +101,9 @@ class DisorderedParallelActionsObservationLists(ObservationLists): The list of all actions used in the traces given (no duplicates). cross_actions (List[ActionPair]): The list of all possible `ActionPairs`. + denominator (float): + The value used for the denominator in all probability calculations (stored so it doesn't need to be recalculated + each time). probabilities (Dict[ActionPair, float]): A dictionary that contains a mapping of each possible `ActionPair` and the probability that the actions in them are disordered. @@ -119,6 +124,7 @@ def __init__(self, traces: TraceList, Token: Type[Observation], features: List[C Any extra arguments to be supplied to the Token __init__. """ self.traces = [] + self.type = Token self.features = features self.learned_theta = learned_theta actions = {step.action for trace in traces for step in trace if step.action} @@ -126,11 +132,14 @@ def __init__(self, traces: TraceList, Token: Type[Observation], features: List[C self.actions = list(actions) # create |A| (action x action set, no duplicates) self.cross_actions = [ActionPair({self.actions[i], self.actions[j]}) for i in range(len(self.actions)) for j in range(i + 1, len(self.actions))] + self.denominator = self._calculate_denom() # dictionary that holds the probabilities of all actions being disordered self.probabilities = self._calculate_all_probabilities() self.tokenize(traces, Token, **kwargs) - def _theta_dot_features_calc(self, f_vec: List[float], theta_vec: List[float]): + + @staticmethod + def _theta_dot_features_calc(f_vec: List[float], theta_vec: List[float]): """Calculate the dot product of the feature vector and the theta vector, then use that as an exponent for 'e'. @@ -145,6 +154,12 @@ def _theta_dot_features_calc(self, f_vec: List[float], theta_vec: List[float]): """ return exp(dot(f_vec, theta_vec)) + def _calculate_denom(self): + denominator = 0 + for combo in self.cross_actions: + denominator += self._theta_dot_features_calc(self._get_f_vec(*combo.tup()), self.learned_theta) + return denominator + def _get_f_vec(self, act_x: Action, act_y: Action): """Returns the feature vector. @@ -173,12 +188,8 @@ def _calculate_probability(self, act_x: Action, act_y: Action): """ # calculate the probability of two given actions being disordered f_vec = self._get_f_vec(act_x, act_y) - theta_vec = self.learned_theta - numerator = self._theta_dot_features_calc(f_vec, theta_vec) - denominator = 0 - for combo in self.cross_actions: - denominator += self._theta_dot_features_calc(self._get_f_vec(*combo.tup()), theta_vec) - return numerator/denominator + numerator = self._theta_dot_features_calc(f_vec, self.learned_theta) + return numerator/self.denominator def _calculate_all_probabilities(self): """Calculates the probabilities of all combinations of actions being disordered. diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 09efa762..11051440 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -23,9 +23,9 @@ def test_tokenization_error(): # TODO: replace with a domain-specific random trace generator traces = RandomGoalSampling( - prob=prob, - dom=dom, - #problem_id=2337, + # prob=prob, + # dom=dom, + problem_id=2337, observe_pres_effs=True, num_traces=3, steps_deep=30, @@ -43,3 +43,5 @@ def test_tokenization_error(): percent_missing=0.10, percent_noisy=0.05, ) + model = Extract(observations, modes.AMDN, occ_threshold = 3) + print(model.details()) From 7099f168eaef285b9b4335a9d0700995d066f4f3 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 19 Aug 2021 17:50:18 -0400 Subject: [PATCH 108/181] Replace logging.warn with warnings.warn --- macq/extract/arms.py | 2 +- macq/observation/observation.py | 2 +- macq/observation/partial_observation.py | 2 +- macq/trace/observation_lists.py | 2 +- macq/trace/trace_list.py | 2 +- tests/extract/test_extract.py | 3 +-- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index f4af667a..acea139d 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,6 +1,6 @@ from collections import defaultdict, Counter from dataclasses import dataclass -from logging import warn +from warnings import warn from typing import Set, List, Dict, Tuple, Hashable from nnf import Var, And, Or, false as nnffalse from pysat.examples.rc2 import RC2 diff --git a/macq/observation/observation.py b/macq/observation/observation.py index a48ab6ce..03a335c2 100644 --- a/macq/observation/observation.py +++ b/macq/observation/observation.py @@ -1,4 +1,4 @@ -from logging import warn +from warnings import warn from json import dumps from typing import Union diff --git a/macq/observation/partial_observation.py b/macq/observation/partial_observation.py index f4285756..eac539ad 100644 --- a/macq/observation/partial_observation.py +++ b/macq/observation/partial_observation.py @@ -1,4 +1,4 @@ -from logging import warn +from warnings import warn from rich.console import Console from ..utils import PercentError diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index 4a175b4e..355697f9 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -1,5 +1,5 @@ from collections import defaultdict -from logging import warn +from warnings import warn from typing import Callable, Dict, List, Type, Set, Union from inspect import cleandoc from rich.console import Console diff --git a/macq/trace/trace_list.py b/macq/trace/trace_list.py index a8de2592..e2de43de 100644 --- a/macq/trace/trace_list.py +++ b/macq/trace/trace_list.py @@ -1,4 +1,4 @@ -from logging import warn +from warnings import warn from typing import List, Callable, Type, Optional from rich.console import Console from . import Action, Trace diff --git a/tests/extract/test_extract.py b/tests/extract/test_extract.py index 1ff02e9f..fa8fc4fe 100644 --- a/tests/extract/test_extract.py +++ b/tests/extract/test_extract.py @@ -3,8 +3,7 @@ from macq.observation import Observation from tests.utils.test_traces import blocks_world -# Other functionality of extract is implicitly tested by any extraction technique -# This is reflected in coverage reports +# Other functionality of extract is tested by extraction technique tests def test_incompatible_observation_token(): From 885a4e59f356aeed11397e3ae64f81810b791416 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 19 Aug 2021 17:56:08 -0400 Subject: [PATCH 109/181] Fix test_trace --- tests/trace/test_trace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/trace/test_trace.py b/tests/trace/test_trace.py index 6e4c9be3..c21eb43d 100644 --- a/tests/trace/test_trace.py +++ b/tests/trace/test_trace.py @@ -61,7 +61,7 @@ def test_trace_get_sas_triples(): (state2, state3) = (trace.steps[1].state, trace.steps[2].state) assert isinstance(action2, Action) - assert trace.get_sas_triples(action2) == {SAS(state2, action2, state3)} + assert trace.get_sas_triples(action2) == [SAS(state2, action2, state3)] # test that the total cost is working correctly From 05bf5a15d6fa83748a95b9937a02120095734de6 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 20 Aug 2021 12:40:37 -0400 Subject: [PATCH 110/181] Update test_arms --- tests/extract/test_arms.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/tests/extract/test_arms.py b/tests/extract/test_arms.py index 236d6474..5af6d1d9 100644 --- a/tests/extract/test_arms.py +++ b/tests/extract/test_arms.py @@ -1,54 +1,51 @@ +from pathlib import Path +from typing import List from macq.trace import * from macq.extract import Extract, modes from macq.observation import PartialObservation from macq.generate.pddl import * -def get_fluent(name: str, objs: list[str]): +def get_fluent(name: str, objs: List[str]): objects = [PlanningObject(o.split()[0], o.split()[1]) for o in objs] return Fluent(name, objects) -if __name__ == "__main__": +def test_arms(): + base = Path(__file__).parent + dom = str((base / "tests/pddl_testing_files/blocks_domain.pddl").resolve()) + prob = str((base / "tests/pddl_testing_files/blocks_problem.pddl").resolve()) + traces = TraceList() - generator = TraceFromGoal(problem_id=1801) - # for f in generator.trace.fluents: - # print(f) + generator = TraceFromGoal(dom=dom, prob=prob) generator.change_goal( { - get_fluent("communicated_soil_data", ["waypoint waypoint2"]), - get_fluent("communicated_rock_data", ["waypoint waypoint3"]), - get_fluent( - "communicated_image_data", ["objective objective1", "mode high_res"] - ), + get_fluent("on", ["object a", "object b"]), + get_fluent("on", ["object b", "object c"]), } ) traces.append(generator.generate_trace()) generator.change_goal( { - get_fluent("communicated_soil_data", ["waypoint waypoint0"]), - get_fluent("communicated_rock_data", ["waypoint waypoint1"]), - get_fluent( - "communicated_image_data", ["objective objective1", "mode high_res"] - ), + get_fluent("on", ["object b", "object a"]), + get_fluent("on", ["object c", "object b"]), } ) traces.append(generator.generate_trace()) - # traces.print("color") observations = traces.tokenize(PartialObservation, percent_missing=0.5) - # model = Extract(observations, modes.ARMS, upper_bound=2, debug=True) model = Extract( observations, modes.ARMS, debug=False, - upper_bound=5, + upper_bound=2, min_support=2, action_weight=110, info_weight=100, - threshold=0.66, + threshold=0.6, info3_default=30, plan_default=30, ) - print(model.details()) + + assert model From a9fbfc544e5131c0a505a6531532256bc2b985c8 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 20 Aug 2021 13:00:05 -0400 Subject: [PATCH 111/181] Fix tests --- macq/observation/atomic_partial_observation.py | 6 +++--- tests/extract/test_amdn.py | 13 +++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/macq/observation/atomic_partial_observation.py b/macq/observation/atomic_partial_observation.py index 6a48f105..39f8cb9e 100644 --- a/macq/observation/atomic_partial_observation.py +++ b/macq/observation/atomic_partial_observation.py @@ -1,6 +1,6 @@ from logging import warning from ..trace import Step, Fluent -from . import PartialObservation +from . import PartialObservation, Observation from typing import Set @@ -42,10 +42,10 @@ def __init__( if percent_missing == 0 and not hide: warning("Creating a PartialObseration with no missing information.") - self.index = step.index + Observation.__init__(self, index=step.index) if percent_missing < 1: - step = self.random_subset(step, percent_missing) + step = self.hide_random_subset(step, percent_missing) if hide: step = self.hide_subset(step, hide) diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 418e87cd..a06b86a8 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -1,4 +1,8 @@ -from macq.trace.disordered_parallel_actions_observation_lists import default_theta_vec, num_parameters_feature, objects_shared_feature +from macq.trace.disordered_parallel_actions_observation_lists import ( + default_theta_vec, + num_parameters_feature, + objects_shared_feature, +) from macq.utils.tokenization_errors import TokenizationError from tests.utils.generators import generate_blocks_traces from macq.extract import Extract, modes @@ -9,6 +13,7 @@ from pathlib import Path import pytest + def test_tokenization_error(): with pytest.raises(TokenizationError): trace = generate_blocks_traces(3)[0] @@ -25,12 +30,12 @@ def test_tokenization_error(): traces = RandomGoalSampling( prob=prob, dom=dom, - #problem_id=2337, + # problem_id=2337, observe_pres_effs=True, num_traces=1, steps_deep=10, subset_size_perc=0.1, - enforced_hill_climbing_sampling=True + enforced_hill_climbing_sampling=True, ).traces features = [objects_shared_feature, num_parameters_feature] @@ -42,4 +47,4 @@ def test_tokenization_error(): learned_theta=learned_theta, percent_missing=0.10, percent_noisy=0.05, - ) \ No newline at end of file + ) From 9533dd5226dc02b0ec222bd8c9d19bdca5c7e2d7 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Fri, 20 Aug 2021 13:07:42 -0400 Subject: [PATCH 112/181] add repr to actionpair, add all_states attribute --- .gitignore | 3 ++- macq/extract/amdn.py | 10 +++++----- ...isordered_parallel_actions_observation_lists.py | 14 ++++++++++++++ tests/extract/test_amdn.py | 14 +++++++------- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index eae90acb..e16fdf39 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ __init__.pyc generated_testing_files/ new_domain.pddl new_prob.pddl -test_model.json \ No newline at end of file +test_model.json +results.txt diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index ede6f5a4..a3adf468 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -203,7 +203,7 @@ def _noise_constraints_6(obs_lists: ObservationLists, all_occ: int, occ_threshol # if the # of occurrences is higher than the user-provided threshold: if occ_r > occ_threshold: # set constraint 6 with the calculated weight - noise_constraints_6[AMDN._or_refactor(~delete(r, a))] = (occ_r / all_occ) + noise_constraints_6[AMDN._or_refactor(~delete(r, a))] = (occ_r / all_occ) * WMAX return noise_constraints_6 @staticmethod @@ -214,7 +214,7 @@ def _noise_constraints_7(obs_lists: ObservationLists, all_occ: int): for r in obs_lists.propositions: occurrences[r] = 0 - for trace in obs_lists.states: + for trace in obs_lists.all_states: for state in trace: true_prop = [r for r in state if state[r]] for r in true_prop: @@ -224,14 +224,14 @@ def _noise_constraints_7(obs_lists: ObservationLists, all_occ: int): for i in range(len(obs_lists.all_par_act_sets)): # get the next trace/states par_act_sets = obs_lists.all_par_act_sets[i] - states = obs_lists.states[i] + states = obs_lists.all_states[i] # iterate through all parallel action sets within the trace for j in range(len(par_act_sets)): # examine the states before and after each parallel action set; set constraints accordinglly true_prop = [r for r in states[j + 1] if states[j + 1][r]] for r in true_prop: if not states[j][r]: - noise_constraints_7[Or([add(r, act) for act in par_act_sets[j]])] = occurrences[r]/all_occ + noise_constraints_7[Or([add(r, act) for act in par_act_sets[j]])] = (occurrences[r]/all_occ) * WMAX return noise_constraints_7 @@ -257,7 +257,7 @@ def _noise_constraints_8(obs_lists, all_occ: int, occ_threshold: int): # if the # of occurrences is higher than the user-provided threshold: if occ_r > occ_threshold: # set constraint 8 with the calculated weight - noise_constraints_8[AMDN._or_refactor(pre(r, a))] = (occ_r / all_occ) + noise_constraints_8[AMDN._or_refactor(pre(r, a))] = (occ_r / all_occ) * WMAX return noise_constraints_8 @staticmethod diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index ccee9728..a69f5154 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -24,6 +24,12 @@ def __hash__(self): sum += hash(a.details()) return sum + def __repr__(self): + string = "" + for a in self.actions: + string += a.details() + ", " + return string[:-1] + def default_theta_vec(k : int): """Generate the default theta vector to be used in the calculation that extracts the probability of actions being disordered; used to "weight" the features. @@ -95,12 +101,16 @@ class DisorderedParallelActionsObservationLists(ObservationLists): The type of token to be used. all_par_act_sets (List[List[Set[Action]]]): Holds the parallel action sets for all traces. + all_states (List(List[State])): + Holds the states for all traces. features (List[Callable]): The list of functions to be used to create the feature vector. learned_theta (List[float]): The supplied theta vector. actions (List[Action]): The list of all actions used in the traces given (no duplicates). + propositions (Set[Fluent]): + The set of all fluents. cross_actions (List[ActionPair]): The list of all possible `ActionPairs`. denominator (float): @@ -128,11 +138,14 @@ def __init__(self, traces: TraceList, Token: Type[Observation], features: List[C self.traces = [] self.type = Token self.all_par_act_sets = [] + self.all_states = [] self.features = features self.learned_theta = learned_theta actions = {step.action for trace in traces for step in trace if step.action} # cast to list for iteration purposes self.actions = list(actions) + # set of all fluents + self.propositions = {f for trace in traces for step in trace for f in step.state.fluents} # create |A| (action x action set, no duplicates) self.cross_actions = [ActionPair({self.actions[i], self.actions[j]}) for i in range(len(self.actions)) for j in range(i + 1, len(self.actions))] self.denominator = self._calculate_denom() @@ -270,6 +283,7 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): par_act_sets[j].discard(act_y) par_act_sets[j].add(act_x) self.all_par_act_sets.append(par_act_sets) + self.all_states.append(states) tokens = [] for i in range(len(par_act_sets)): for act in par_act_sets[i]: diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 11051440..b39bcde1 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -5,7 +5,6 @@ from macq.generate.pddl import RandomGoalSampling from macq.observation import * from macq.trace import * - from pathlib import Path import pytest @@ -23,12 +22,12 @@ def test_tokenization_error(): # TODO: replace with a domain-specific random trace generator traces = RandomGoalSampling( - # prob=prob, - # dom=dom, - problem_id=2337, + prob=prob, + dom=dom, + #problem_id=2337, observe_pres_effs=True, - num_traces=3, - steps_deep=30, + num_traces=10, + steps_deep=50, subset_size_perc=0.1, enforced_hill_climbing_sampling=True ).traces @@ -44,4 +43,5 @@ def test_tokenization_error(): percent_noisy=0.05, ) model = Extract(observations, modes.AMDN, occ_threshold = 3) - print(model.details()) + f = open("results.txt", "w") + f.write(model.details()) From dd63a062573bfc8e21817d3cd1053d9edf328a1a Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 20 Aug 2021 13:12:51 -0400 Subject: [PATCH 113/181] Fix path to test pddl --- tests/extract/test_arms.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/extract/test_arms.py b/tests/extract/test_arms.py index 5af6d1d9..ae9ec82e 100644 --- a/tests/extract/test_arms.py +++ b/tests/extract/test_arms.py @@ -12,9 +12,9 @@ def get_fluent(name: str, objs: List[str]): def test_arms(): - base = Path(__file__).parent - dom = str((base / "tests/pddl_testing_files/blocks_domain.pddl").resolve()) - prob = str((base / "tests/pddl_testing_files/blocks_problem.pddl").resolve()) + base = Path(__file__).parent.parent + dom = str((base / "pddl_testing_files/blocks_domain.pddl").resolve()) + prob = str((base / "pddl_testing_files/blocks_problem.pddl").resolve()) traces = TraceList() generator = TraceFromGoal(dom=dom, prob=prob) From 10f8833b2b164d3b0597e55d78ef4c375dba53ae Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 20 Aug 2021 13:25:52 -0400 Subject: [PATCH 114/181] Add test for DisorderedParallelActionsObservationLists --- tests/extract/test_amdn.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index a06b86a8..0adfc186 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -20,6 +20,39 @@ def test_tokenization_error(): trace.tokenize(Token=NoisyPartialDisorderedParallelObservation) +def test_observations(): + base = Path(__file__).parent.parent + dom = str((base / "pddl_testing_files/blocks_domain.pddl").resolve()) + prob = str((base / "pddl_testing_files/blocks_problem.pddl").resolve()) + + # TODO: replace with a domain-specific random trace generator + traces = RandomGoalSampling( + prob=prob, + dom=dom, + # problem_id=2337, + observe_pres_effs=True, + num_traces=1, + steps_deep=10, + subset_size_perc=0.1, + enforced_hill_climbing_sampling=True, + ).traces + + print(traces.traces) + + features = [objects_shared_feature, num_parameters_feature] + learned_theta = default_theta_vec(2) + observations = traces.tokenize( + Token=NoisyPartialDisorderedParallelObservation, + ObsLists=DisorderedParallelActionsObservationLists, + features=features, + learned_theta=learned_theta, + percent_missing=0.10, + percent_noisy=0.05, + ) + + assert observations.traces + + if __name__ == "__main__": # exit out to the base macq folder so we can get to /tests base = Path(__file__).parent.parent From 2ae971148a51b9ebd614cf870493e91516fd7f20 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 20 Aug 2021 14:01:10 -0400 Subject: [PATCH 115/181] Restore trace Action effects --- macq/observation/noisy_observation.py | 9 +++------ macq/observation/partial_observation.py | 4 ++-- macq/trace/action.py | 24 ++++++++++++++++++++---- tests/extract/test_amdn.py | 2 -- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/macq/observation/noisy_observation.py b/macq/observation/noisy_observation.py index ab09ebdd..7e61ef94 100644 --- a/macq/observation/noisy_observation.py +++ b/macq/observation/noisy_observation.py @@ -1,6 +1,6 @@ from . import Observation from ..trace import Step -from ..utils import PercentError#, extract_fluent_subset +from ..utils import PercentError # , extract_fluent_subset class NoisyObservation(Observation): @@ -12,8 +12,7 @@ class NoisyObservation(Observation): This token can be used to create states that are noisy but fully observable. """ - def __init__( - self, step: Step, percent_noisy: float = 0): + def __init__(self, step: Step, percent_noisy: float = 0): """ Creates an NoisyObservation object, storing the state and action. @@ -27,7 +26,7 @@ def __init__( super().__init__(index=step.index) if percent_noisy > 1 or percent_noisy < 0: - raise PercentError() + raise PercentError() step = self.random_noisy_subset(step, percent_noisy) @@ -57,5 +56,3 @@ def random_noisy_subset(self, step: Step, percent_noisy: float): for f in state: state[f] = not state[f] if f in noisy_f else state[f] return Step(state, step.action, step.index) - - diff --git a/macq/observation/partial_observation.py b/macq/observation/partial_observation.py index 7c4088fa..163a69d4 100644 --- a/macq/observation/partial_observation.py +++ b/macq/observation/partial_observation.py @@ -33,8 +33,8 @@ def __init__( if percent_missing == 0 and not hide: warning("Creating a PartialObseration with no missing information.") - # Observation.__init__(self, index=step.index) - super().__init__(index=step.index) + # NOTE: Can't use super due to multiple inheritence (NoisyPartialObservation) + Observation.__init__(self, index=step.index) # If percent_missing == 1 -> self.state = None (below). # This allows ARMS (and other algorithms) to skip steps when there is no diff --git a/macq/trace/action.py b/macq/trace/action.py index ef22d82f..1a090d4a 100644 --- a/macq/trace/action.py +++ b/macq/trace/action.py @@ -1,5 +1,5 @@ -from typing import List -from .fluent import PlanningObject +from typing import List, Set +from .fluent import PlanningObject, Fluent class Action: @@ -18,11 +18,18 @@ class Action: The cost to perform the action. """ - def __init__(self, name: str, obj_params: List[PlanningObject], cost: int = 0): + def __init__( + self, + name: str, + obj_params: List[PlanningObject], + cost: int = 0, + precond: Set[Fluent] = None, + add: Set[Fluent] = None, + delete: Set[Fluent] = None, + ): """Initializes an Action with the parameters provided. The `precond`, `add`, and `delete` args should only be provided in Model deserialization. - Args: name (str): The name of the action. @@ -30,10 +37,19 @@ def __init__(self, name: str, obj_params: List[PlanningObject], cost: int = 0): The list of objects the action acts on. cost (int): Optional; The cost to perform the action. Defaults to 0. + precond (Set[Fluent]): + Optional; The set of Fluents that make up the precondition. + add (Set[Fluent]): + Optional; The set of Fluents that make up the add effects. + delete (Set[Fluent]): + Optional; The set of Fluents that make up the delete effects. """ self.name = name self.obj_params = obj_params self.cost = cost + self.precond = precond + self.add = add + self.delete = delete def __str__(self): string = f"{self.name} {' '.join(map(str, self.obj_params))}" diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 0adfc186..66cdfd59 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -37,8 +37,6 @@ def test_observations(): enforced_hill_climbing_sampling=True, ).traces - print(traces.traces) - features = [objects_shared_feature, num_parameters_feature] learned_theta = default_theta_vec(2) observations = traces.tokenize( From 70a51cea4979a565f6790be57660a406819cd4e6 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Fri, 20 Aug 2021 16:57:27 -0400 Subject: [PATCH 116/181] make test tracelist for debugging --- tests/extract/test_amdn.py | 98 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 5 deletions(-) diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index b39bcde1..b3736771 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -2,7 +2,7 @@ from macq.utils.tokenization_errors import TokenizationError from tests.utils.generators import generate_blocks_traces from macq.extract import Extract, modes -from macq.generate.pddl import RandomGoalSampling +from macq.generate.pddl import RandomGoalSampling, Generator from macq.observation import * from macq.trace import * from pathlib import Path @@ -13,6 +13,88 @@ def test_tokenization_error(): trace = generate_blocks_traces(3)[0] trace.tokenize(Token=NoisyPartialDisorderedParallelObservation) +def test_tracelist(): + # define objects + red_truck = PlanningObject("object", "red_truck") + blue_truck = PlanningObject("object", "blue_truck") + location_a = PlanningObject("object", "location_a") + location_b = PlanningObject("object", "location_b") + location_c = PlanningObject("object", "location_c") + location_d = PlanningObject("object", "location_d") + + red_truck_is_truck = Fluent("truck", [red_truck]) + blue_truck_is_truck = Fluent("truck", [blue_truck]) + location_a_is_place = Fluent("place", [location_a]) + location_b_is_place = Fluent("place", [location_b]) + location_c_is_place = Fluent("place", [location_c]) + location_d_is_place = Fluent("place", [location_d]) + red_at_a = Fluent("at", [red_truck, location_a]) + red_at_b = Fluent("at", [red_truck, location_b]) + blue_at_c = Fluent("at", [blue_truck, location_c]) + blue_at_d = Fluent("at", [blue_truck, location_d]) + blue_at_a = Fluent("at", [blue_truck, location_a]) + blue_at_b = Fluent("at", [blue_truck, location_b]) + + drive_red_a_b = Action("drive", [red_truck, location_a, location_b], precond={red_truck_is_truck, location_a_is_place, location_b_is_place, red_at_a}, add={red_at_b}, delete={red_at_a}) + drive_blue_c_d = Action("drive", [blue_truck, location_c, location_d], precond={blue_truck_is_truck, location_c_is_place, location_d_is_place, blue_at_c}, add={blue_at_d}, delete={blue_at_c}) + + + step_0 = Step( + State( + { + red_truck_is_truck: True, + blue_truck_is_truck: True, + location_a_is_place: True, + location_b_is_place: True, + location_c_is_place: True, + location_d_is_place: True, + red_at_a: True, + red_at_b: False, + blue_at_c: True, + blue_at_d: False, + }), + drive_red_a_b, + 0 + ) + + step_1 = Step( + State( + { + red_truck_is_truck: True, + blue_truck_is_truck: True, + location_a_is_place: True, + location_b_is_place: True, + location_c_is_place: True, + location_d_is_place: True, + red_at_a: False, + red_at_b: True, + blue_at_c: True, + blue_at_d: False, + }), + drive_blue_c_d, + 1 + ) + + step_2 = Step( + State( + { + red_truck_is_truck: True, + blue_truck_is_truck: True, + location_a_is_place: True, + location_b_is_place: True, + location_c_is_place: True, + location_d_is_place: True, + red_at_a: False, + red_at_b: True, + blue_at_c: False, + blue_at_d: True, + }), + None, + 2 + ) + + test_trace_1 = Trace([step_0, step_1, step_2]) + return TraceList([test_trace_1]) if __name__ == "__main__": # exit out to the base macq folder so we can get to /tests @@ -20,17 +102,23 @@ def test_tokenization_error(): dom = str((base / "pddl_testing_files/blocks_domain.pddl").resolve()) prob = str((base / "pddl_testing_files/blocks_problem.pddl").resolve()) + traces = test_tracelist() + + """ # TODO: replace with a domain-specific random trace generator traces = RandomGoalSampling( - prob=prob, - dom=dom, - #problem_id=2337, + # prob=prob, + # dom=dom, + problem_id=2337, observe_pres_effs=True, - num_traces=10, + num_traces=1, steps_deep=50, subset_size_perc=0.1, enforced_hill_climbing_sampling=True ).traces + traces.print() + """ + features = [objects_shared_feature, num_parameters_feature] learned_theta = default_theta_vec(2) From 60906b147debb44dcafc1eff14f78f2df8ca3415 Mon Sep 17 00:00:00 2001 From: ecal Date: Fri, 20 Aug 2021 17:00:54 -0400 Subject: [PATCH 117/181] Remove Disordered... test --- tests/extract/test_amdn.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 66cdfd59..a06b86a8 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -20,37 +20,6 @@ def test_tokenization_error(): trace.tokenize(Token=NoisyPartialDisorderedParallelObservation) -def test_observations(): - base = Path(__file__).parent.parent - dom = str((base / "pddl_testing_files/blocks_domain.pddl").resolve()) - prob = str((base / "pddl_testing_files/blocks_problem.pddl").resolve()) - - # TODO: replace with a domain-specific random trace generator - traces = RandomGoalSampling( - prob=prob, - dom=dom, - # problem_id=2337, - observe_pres_effs=True, - num_traces=1, - steps_deep=10, - subset_size_perc=0.1, - enforced_hill_climbing_sampling=True, - ).traces - - features = [objects_shared_feature, num_parameters_feature] - learned_theta = default_theta_vec(2) - observations = traces.tokenize( - Token=NoisyPartialDisorderedParallelObservation, - ObsLists=DisorderedParallelActionsObservationLists, - features=features, - learned_theta=learned_theta, - percent_missing=0.10, - percent_noisy=0.05, - ) - - assert observations.traces - - if __name__ == "__main__": # exit out to the base macq folder so we can get to /tests base = Path(__file__).parent.parent From ee2daf8c96d4a7de4b53383437afdb90b6ca7abe Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 23 Aug 2021 11:42:11 -0400 Subject: [PATCH 118/181] Cleanup Action --- macq/trace/action.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/macq/trace/action.py b/macq/trace/action.py index 1a090d4a..bdc75cf8 100644 --- a/macq/trace/action.py +++ b/macq/trace/action.py @@ -16,6 +16,12 @@ class Action: The list of objects the action acts on. cost (int): The cost to perform the action. + precond (Set[Fluent]): + The set of Fluents that make up the precondition. + add (Set[Fluent]): + The set of Fluents that make up the add effects. + delete (Set[Fluent]): + The set of Fluents that make up the delete effects. """ def __init__( @@ -78,15 +84,6 @@ def clone(self, atomic=False): return Action(self.name, self.obj_params.copy(), self.cost) - def add_parameter(self, obj: PlanningObject): - """Adds an object to the action's parameters. - - Args: - obj (PlanningObject): - The object to be added to the action's object parameters. - """ - self.obj_params.append(obj) - def _serialize(self): return self.name From eb8a821bb6c1a337783fcb26bf8bf5d116884ca9 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 23 Aug 2021 11:43:16 -0400 Subject: [PATCH 119/181] Cleanup NoisyObservation --- macq/observation/noisy_observation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macq/observation/noisy_observation.py b/macq/observation/noisy_observation.py index 7e61ef94..b4b9ff6a 100644 --- a/macq/observation/noisy_observation.py +++ b/macq/observation/noisy_observation.py @@ -1,6 +1,6 @@ from . import Observation from ..trace import Step -from ..utils import PercentError # , extract_fluent_subset +from ..utils import PercentError class NoisyObservation(Observation): From c44980739b1092788a1a2bcb2cd3af8fc96709e0 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 23 Aug 2021 11:49:11 -0400 Subject: [PATCH 120/181] Replace if with return in all __eq__ functions --- macq/extract/learned_action.py | 9 +++++---- macq/extract/learned_fluent.py | 5 ++--- macq/extract/model.py | 14 +++++++++----- macq/generate/plan.py | 5 +++-- macq/observation/atomic_partial_observation.py | 8 +++++--- macq/observation/identity_observation.py | 8 +++++--- macq/observation/partial_observation.py | 8 +++++--- macq/trace/state.py | 4 +--- 8 files changed, 35 insertions(+), 26 deletions(-) diff --git a/macq/extract/learned_action.py b/macq/extract/learned_action.py index d11d5f7f..89b7ae47 100644 --- a/macq/extract/learned_action.py +++ b/macq/extract/learned_action.py @@ -1,6 +1,5 @@ from __future__ import annotations from typing import Set, List -from ..trace import Fluent class LearnedAction: @@ -15,9 +14,11 @@ def __init__(self, name: str, obj_params: List, **kwargs): self.delete = set() if "delete" not in kwargs else kwargs["delete"] def __eq__(self, other): - if not isinstance(other, LearnedAction): - return False - return self.name == other.name and self.obj_params == other.obj_params + return ( + isinstance(other, LearnedAction) + and self.name == other.name + and self.obj_params == other.obj_params + ) def __hash__(self): # Order of obj_params is important! diff --git a/macq/extract/learned_fluent.py b/macq/extract/learned_fluent.py index c80d3545..7093bd73 100644 --- a/macq/extract/learned_fluent.py +++ b/macq/extract/learned_fluent.py @@ -1,14 +1,13 @@ from typing import List + class LearnedFluent: def __init__(self, name: str, objects: List): self.name = name self.objects = objects def __eq__(self, other): - if not isinstance(other, LearnedFluent): - return False - return hash(self) == hash(other) + return isinstance(other, LearnedFluent) and hash(self) == hash(other) def __hash__(self): # Order of objects is important! diff --git a/macq/extract/model.py b/macq/extract/model.py index 6a2fc578..04af2c05 100644 --- a/macq/extract/model.py +++ b/macq/extract/model.py @@ -27,9 +27,7 @@ class Model: action attributes characterize the model. """ - def __init__( - self, fluents: Set[LearnedFluent], actions: Set[LearnedAction] - ): + def __init__(self, fluents: Set[LearnedFluent], actions: Set[LearnedAction]): """Initializes a Model with a set of fluents and a set of actions. Args: @@ -129,7 +127,13 @@ def __to_tarski_formula(self, attribute: Set[str], lang: FirstOrderLanguage): Connective.And, [lang.get(a.replace(" ", "_"))() for a in attribute] ) - def to_pddl(self, domain_name: str, problem_name: str, domain_filename: str, problem_filename: str): + def to_pddl( + self, + domain_name: str, + problem_name: str, + domain_filename: str, + problem_filename: str, + ): """Dumps a Model to two PDDL files. The conversion only uses 0-arity predicates, and no types, objects, or parameters of any kind are used. Actions are represented as ground actions with no parameters. @@ -192,4 +196,4 @@ def deserialize(string: str): @classmethod def _from_json(cls, data: dict): actions = set(map(LearnedAction._deserialize, data["actions"])) - return cls(set(data["fluents"]), actions) \ No newline at end of file + return cls(set(data["fluents"]), actions) diff --git a/macq/generate/plan.py b/macq/generate/plan.py index 8837c60a..6b8838a4 100644 --- a/macq/generate/plan.py +++ b/macq/generate/plan.py @@ -1,6 +1,7 @@ from typing import List from tarski.fstrips.action import PlainOperator + class Plan: """A Plan. @@ -11,6 +12,7 @@ class Plan: actions (List[PlainOperator]): The list of actions that make up the plan. """ + def __init__(self, actions: List[PlainOperator]): """Creates a Plan by instantiating it with the list of actions (of tarski type `PlainOperator`). @@ -42,5 +44,4 @@ def __str__(self): return "\n".join(string) def __eq__(self, other): - if isinstance(other, Plan): - return self.actions == other.actions \ No newline at end of file + return isinstance(other, Plan) and self.actions == other.actions diff --git a/macq/observation/atomic_partial_observation.py b/macq/observation/atomic_partial_observation.py index 39f8cb9e..b711f9e5 100644 --- a/macq/observation/atomic_partial_observation.py +++ b/macq/observation/atomic_partial_observation.py @@ -53,9 +53,11 @@ def __init__( self.action = None if step.action is None else step.action.clone(atomic=True) def __eq__(self, other): - if not isinstance(other, AtomicPartialObservation): - return False - return self.state == other.state and self.action == other.action + return ( + isinstance(other, AtomicPartialObservation) + and self.state == other.state + and self.action == other.action + ) def details(self): return f"Obs {str(self.index)}.\n State: {str(self.state)}\n Action: {str(self.action)}" diff --git a/macq/observation/identity_observation.py b/macq/observation/identity_observation.py index 16b3dd5c..f864cf50 100644 --- a/macq/observation/identity_observation.py +++ b/macq/observation/identity_observation.py @@ -45,9 +45,11 @@ def __hash__(self): return hash(self.details()) def __eq__(self, other): - if not isinstance(other, IdentityObservation): - return False - return self.state == other.state and self.action == other.action + return ( + isinstance(other, IdentityObservation) + and self.state == other.state + and self.action == other.action + ) def details(self): return f"Obs {str(self.index)}.\n State: {str(self.state)}\n Action: {str(self.action)}" diff --git a/macq/observation/partial_observation.py b/macq/observation/partial_observation.py index 163a69d4..ca56c132 100644 --- a/macq/observation/partial_observation.py +++ b/macq/observation/partial_observation.py @@ -49,9 +49,11 @@ def __init__( self.action = None if step.action is None else step.action.clone() def __eq__(self, other): - if not isinstance(other, PartialObservation): - return False - return self.state == other.state and self.action == other.action + return ( + isinstance(other, PartialObservation) + and self.state == other.state + and self.action == other.action + ) def hide_random_subset(self, step: Step, percent_missing: float): """Hides a random subset of the fluents in the step. diff --git a/macq/trace/state.py b/macq/trace/state.py index 3f1216a3..8378d621 100644 --- a/macq/trace/state.py +++ b/macq/trace/state.py @@ -26,9 +26,7 @@ def __init__(self, fluents: Dict[Fluent, bool] = None): self.fluents = fluents if fluents is not None else {} def __eq__(self, other): - if not isinstance(other, State): - return False - return self.fluents == other.fluents + return isinstance(other, State) and self.fluents == other.fluents def __str__(self): return ", ".join([str(fluent) for (fluent, value) in self.items() if value]) From 71cf99e27a0fa3724ba8db1940f7e353fb5a0b5f Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 23 Aug 2021 11:51:45 -0400 Subject: [PATCH 121/181] Fix Action test --- tests/trace/test_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/trace/test_action.py b/tests/trace/test_action.py index 7358ed48..8425a1b4 100644 --- a/tests/trace/test_action.py +++ b/tests/trace/test_action.py @@ -13,5 +13,5 @@ def test_action(): assert str(a1) obj = PlanningObject("test_obj", "test") - a1.add_parameter(obj) + a1.obj_params.append(obj) assert obj in a1.obj_params From 004279803b13ff84ebaf7d88ea3591621cecaf96 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 23 Aug 2021 12:38:00 -0400 Subject: [PATCH 122/181] Add 5x try catch to response from planning.domains solver --- macq/generate/pddl/generator.py | 67 +++++++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/macq/generate/pddl/generator.py b/macq/generate/pddl/generator.py index 6c789a9c..865d605a 100644 --- a/macq/generate/pddl/generator.py +++ b/macq/generate/pddl/generator.py @@ -21,6 +21,14 @@ from ...trace import Action, State, PlanningObject, Fluent, Trace, Step +class PlanningDomainsAPIError(Exception): + """Raised when a valid response cannot be obtained from the planning.domains solver.""" + + def __init__(self, message, e): + self.e = e + super().__init__(message) + + class InvalidGoalFluent(Exception): """ Raised when the user attempts to supply a new goal with invalid fluent(s). @@ -56,10 +64,16 @@ class Generator: op_dict (dict): The problem's ground operators, formatted to a dictionary for easy access during plan generation. observe_pres_effs (bool): - Option to observe action preconditions and effects upon generation. + Option to observe action preconditions and effects upon generation. """ - def __init__(self, dom: str = None, prob: str = None, problem_id: int = None, observe_pres_effs: bool = False): + def __init__( + self, + dom: str = None, + prob: str = None, + problem_id: int = None, + observe_pres_effs: bool = False, + ): """Creates a basic PDDL state trace generator. Takes either the raw filenames of the domain and problem, or a problem ID. @@ -71,7 +85,7 @@ def __init__(self, dom: str = None, prob: str = None, problem_id: int = None, ob problem_id (int): The ID of the problem to access. observe_pres_effs (bool): - Option to observe action preconditions and effects upon generation. + Option to observe action preconditions and effects upon generation. """ # get attributes self.pddl_dom = dom @@ -247,9 +261,7 @@ def tarski_act_to_macq(self, tarski_act: PlainOperator): raw_precond = tarski_act.precondition.subformulas for raw_p in raw_precond: if isinstance(raw_p, CompoundFormula): - precond.add( - self.__tarski_atom_to_macq_fluent(raw_p.subformulas[0]) - ) + precond.add(self.__tarski_atom_to_macq_fluent(raw_p.subformulas[0])) else: precond.add(self.__tarski_atom_to_macq_fluent(raw_p)) else: @@ -262,7 +274,17 @@ def tarski_act_to_macq(self, tarski_act: PlainOperator): for fluent in precond: objs.update(set(fluent.objects)) - return Action(name=name, obj_params=list(objs), precond=precond, add=add, delete=delete) if self.observe_pres_effs else Action(name=name, obj_params=list(objs)) + return ( + Action( + name=name, + obj_params=list(objs), + precond=precond, + add=add, + delete=delete, + ) + if self.observe_pres_effs + else Action(name=name, obj_params=list(objs)) + ) def change_init( self, @@ -371,10 +393,33 @@ def generate_plan(self, from_ipc_file: bool = False, filename: str = None): "domain": open(self.pddl_dom, "r").read(), "problem": open(self.pddl_prob, "r").read(), } - resp = requests.post( - "http://solver.planning.domains/solve", verify=False, json=data - ).json() - plan = [act["name"] for act in resp["result"]["plan"]] + + def get_api_response(): + resp = requests.post( + "http://solver.planning.domains/solve", verify=False, json=data + ).json() + return [act["name"] for act in resp["result"]["plan"]] + + try: + plan = get_api_response() + except TypeError: + try: + plan = get_api_response() + except TypeError: + try: + plan = get_api_response() + except TypeError: + try: + plan = get_api_response() + except TypeError: + try: + plan = get_api_response() + except TypeError as e: + raise PlanningDomainsAPIError( + "Could not get a valid response from the planning.domains solver after 5 attempts.", + e, + ) + else: f = open(filename, "r") plan = list(filter(lambda x: ";" not in x, f.read().splitlines())) From 1a01fec18173532f86174276bca4e4b8451245a0 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 23 Aug 2021 12:49:18 -0400 Subject: [PATCH 123/181] Add sleep to later try-catch attempts --- macq/generate/pddl/generator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/macq/generate/pddl/generator.py b/macq/generate/pddl/generator.py index 865d605a..3249851a 100644 --- a/macq/generate/pddl/generator.py +++ b/macq/generate/pddl/generator.py @@ -1,3 +1,4 @@ +from time import sleep from typing import Set, List, Union from tarski.io import PDDLReader from tarski.search import GroundForwardSearchModel @@ -407,12 +408,15 @@ def get_api_response(): plan = get_api_response() except TypeError: try: + sleep(1) plan = get_api_response() except TypeError: try: + sleep(5) plan = get_api_response() except TypeError: try: + sleep(10) plan = get_api_response() except TypeError as e: raise PlanningDomainsAPIError( From 8307d1cf7a440097af86697bf9f2d9fed6ae5fe7 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Mon, 23 Aug 2021 15:42:11 -0400 Subject: [PATCH 124/181] add repr for debugging purposes --- macq/trace/action.py | 2 +- macq/trace/fluent.py | 5 ++++- tests/extract/test_amdn.py | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/macq/trace/action.py b/macq/trace/action.py index d34bc52f..224c81b2 100644 --- a/macq/trace/action.py +++ b/macq/trace/action.py @@ -58,7 +58,7 @@ def __init__( self.add = add self.delete = delete - def __str__(self): + def __repr__(self): string = f"{self.name} {' '.join(map(str, self.obj_params))}" return string diff --git a/macq/trace/fluent.py b/macq/trace/fluent.py index d1581e3e..ff96f1fd 100644 --- a/macq/trace/fluent.py +++ b/macq/trace/fluent.py @@ -34,6 +34,9 @@ def __eq__(self, other): def details(self): return " ".join([self.obj_type, self.name]) + def __repr__(self): + return self.details() + def _serialize(self): return self.details() @@ -66,7 +69,7 @@ def __hash__(self): # Order of objects is important! return hash(str(self)) - def __str__(self): + def __repr__(self): return f"({self.name} {' '.join([o.details() for o in self.objects])})" def __eq__(self, other): diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index b3736771..0e9a00a5 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -127,8 +127,8 @@ def test_tracelist(): ObsLists=DisorderedParallelActionsObservationLists, features=features, learned_theta=learned_theta, - percent_missing=0.10, - percent_noisy=0.05, + percent_missing=0, + percent_noisy=0, ) model = Extract(observations, modes.AMDN, occ_threshold = 3) f = open("results.txt", "w") From eb311f33d78844d2cee79cd4b2037109f53e9bb1 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Mon, 23 Aug 2021 16:01:54 -0400 Subject: [PATCH 125/181] add last state with empty action to tokens --- .../disordered_parallel_actions_observation_lists.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index a69f5154..b72854e9 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -57,7 +57,7 @@ def objects_shared_feature(act_x: Action, act_y: Action): """ num_shared = 0 for obj in act_x.obj_params: - for other_obj in act_y. obj_params: + for other_obj in act_y.obj_params: if obj == other_obj: num_shared += 1 return num_shared @@ -102,7 +102,8 @@ class DisorderedParallelActionsObservationLists(ObservationLists): all_par_act_sets (List[List[Set[Action]]]): Holds the parallel action sets for all traces. all_states (List(List[State])): - Holds the states for all traces. + Holds the relevant states for all traces. Note that these are RELATIVE to the parallel action sets and only + contain the states between the sets. features (List[Callable]): The list of functions to be used to create the feature vector. learned_theta (List[float]): @@ -287,6 +288,8 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): tokens = [] for i in range(len(par_act_sets)): for act in par_act_sets[i]: - tokens.append(Token(Step(state=states[i], action=act, index=i), par_act_set_ID = i, **kwargs)) - self.append(tokens) + tokens.append(Token(Step(state=states[i], action=act, index=i), par_act_set_ID=i, **kwargs)) + # add the final token, with the final state but no action + tokens.append(Token(Step(state=states[-1], action=None, index=len(par_act_sets)), par_act_set_ID=len(par_act_sets), **kwargs)) + self.append(tokens) \ No newline at end of file From dc70253f8d6278f1dc4a121996fdec5cc2ec663f Mon Sep 17 00:00:00 2001 From: beckydvn Date: Mon, 23 Aug 2021 16:51:00 -0400 Subject: [PATCH 126/181] make larger test trace, accomodate "None" action --- macq/extract/amdn.py | 10 +++-- tests/extract/test_amdn.py | 76 ++++++++++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index a3adf468..3bfd3428 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -244,10 +244,12 @@ def _noise_constraints_8(obs_lists, all_occ: int, occ_threshold: int): for i in range(len(obs_lists)): # iterate through each step in each trace for j in range(len(obs_lists[i])): - true_prop = [f for f in obs_lists[i][j].state if obs_lists[i][j].state[f]] - for r in true_prop: - # count the number of occurrences of each action and its previous proposition - occurrences[obs_lists[i][j].action][r] += 1 + # if the action is not None + if obs_lists[i][j].action: + true_prop = [f for f in obs_lists[i][j].state if obs_lists[i][j].state[f]] + for r in true_prop: + # count the number of occurrences of each action and its previous proposition + occurrences[obs_lists[i][j].action][r] += 1 # iterate through actions for a in occurrences: diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 0e9a00a5..a7bdd5df 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -30,15 +30,22 @@ def test_tracelist(): location_d_is_place = Fluent("place", [location_d]) red_at_a = Fluent("at", [red_truck, location_a]) red_at_b = Fluent("at", [red_truck, location_b]) - blue_at_c = Fluent("at", [blue_truck, location_c]) - blue_at_d = Fluent("at", [blue_truck, location_d]) + red_at_c = Fluent("at", [red_truck, location_c]) + red_at_d = Fluent("at", [red_truck, location_d]) blue_at_a = Fluent("at", [blue_truck, location_a]) blue_at_b = Fluent("at", [blue_truck, location_b]) + blue_at_c = Fluent("at", [blue_truck, location_c]) + blue_at_d = Fluent("at", [blue_truck, location_d]) + + drive_red_a_b = Action("drive", [red_truck, location_a, location_b], precond={red_truck_is_truck, location_a_is_place, location_b_is_place, red_at_a}, add={red_at_b}, delete={red_at_a}) drive_blue_c_d = Action("drive", [blue_truck, location_c, location_d], precond={blue_truck_is_truck, location_c_is_place, location_d_is_place, blue_at_c}, add={blue_at_d}, delete={blue_at_c}) + drive_blue_d_b = Action("drive", [blue_truck, location_d, location_b], precond={blue_truck_is_truck, location_d_is_place, location_b_is_place, blue_at_d}, add={blue_at_b}, delete={blue_at_d}) + drive_red_b_d = Action("drive", [red_truck, location_b, location_d], precond={red_truck_is_truck, location_b_is_place, location_d_is_place, red_at_b}, add={red_at_d}, delete={red_at_b}) + # trace: {red a -> b, blue c -> d}, {blue d -> b}, {red b -> d}, {red d -> a, blue b -> c} step_0 = Step( State( { @@ -50,6 +57,10 @@ def test_tracelist(): location_d_is_place: True, red_at_a: True, red_at_b: False, + red_at_c: False, + red_at_d: False, + blue_at_a: False, + blue_at_b: False, blue_at_c: True, blue_at_d: False, }), @@ -68,6 +79,10 @@ def test_tracelist(): location_d_is_place: True, red_at_a: False, red_at_b: True, + red_at_c: False, + red_at_d: False, + blue_at_a: False, + blue_at_b: False, blue_at_c: True, blue_at_d: False, }), @@ -86,15 +101,62 @@ def test_tracelist(): location_d_is_place: True, red_at_a: False, red_at_b: True, + red_at_c: False, + red_at_d: False, + blue_at_a: False, + blue_at_b: False, blue_at_c: False, blue_at_d: True, }), - None, + drive_blue_d_b, 2 ) - - test_trace_1 = Trace([step_0, step_1, step_2]) - return TraceList([test_trace_1]) + + step_3 = Step( + State( + { + red_truck_is_truck: True, + blue_truck_is_truck: True, + location_a_is_place: True, + location_b_is_place: True, + location_c_is_place: True, + location_d_is_place: True, + red_at_a: False, + red_at_b: True, + red_at_c: False, + red_at_d: False, + blue_at_a: False, + blue_at_b: True, + blue_at_c: False, + blue_at_d: False, + }), + drive_red_b_d, + 3 + ) + + step_4 = Step( + State( + { + red_truck_is_truck: True, + blue_truck_is_truck: True, + location_a_is_place: True, + location_b_is_place: True, + location_c_is_place: True, + location_d_is_place: True, + red_at_a: False, + red_at_b: False, + red_at_c: False, + red_at_d: True, + blue_at_a: False, + blue_at_b: True, + blue_at_c: False, + blue_at_d: False, + }), + None, + 4 + ) + + return TraceList([Trace([step_0, step_1, step_2, step_3, step_4])]) if __name__ == "__main__": # exit out to the base macq folder so we can get to /tests @@ -130,6 +192,6 @@ def test_tracelist(): percent_missing=0, percent_noisy=0, ) - model = Extract(observations, modes.AMDN, occ_threshold = 3) + model = Extract(observations, modes.AMDN, occ_threshold = 1) f = open("results.txt", "w") f.write(model.details()) From 0505825f8beafbd418c24ec9781e3859b9d91ff3 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 24 Aug 2021 09:57:39 -0400 Subject: [PATCH 127/181] Update test_slaf --- tests/extract/test_slaf.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/extract/test_slaf.py b/tests/extract/test_slaf.py index 09083f0d..e7475faa 100644 --- a/tests/extract/test_slaf.py +++ b/tests/extract/test_slaf.py @@ -23,20 +23,19 @@ def test_slaf(): # exit out to the base macq folder so we can get to /tests base = Path(__file__).parent.parent model_blocks_dom = str( - (base / "generated_testing_files/model_blocks_domain.pddl").resolve() + (base / "pddl_testing_files/model_blocks_domain.pddl").resolve() ) model_blocks_prob = str( - (base / "generated_testing_files/model_blocks_problem.pddl").resolve() + (base / "pddl_testing_files/model_blocks_problem.pddl").resolve() ) traces = generate_blocks_traces(plan_len=2, num_traces=1) observations = traces.tokenize( AtomicPartialObservation, - method=AtomicPartialObservation.random_subset, percent_missing=0.10, ) traces.print() - model = Extract(observations, modes.SLAF, debug_mode=True) + model = Extract(observations, modes.SLAF) print(model.details()) model.to_pddl( From cf03bbe03765cf6cb1a85cef2126bc96b944d6fa Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 24 Aug 2021 11:30:01 -0400 Subject: [PATCH 128/181] refactor disorder constraints --- macq/extract/amdn.py | 31 ++++++++++++------- ...ered_parallel_actions_observation_lists.py | 1 + 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 3bfd3428..886f0a97 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -84,32 +84,41 @@ def _build_disorder_constraints(obs_lists: ObservationLists): disorder_constraints = {} # iterate through all traces for i in range(len(obs_lists.all_par_act_sets)): + # get the parallel action sets for this trace par_act_sets = obs_lists.all_par_act_sets[i] # iterate through all pairs of parallel action sets for this trace + # use -1 since we will be referencing the current parallel action set and the following one for j in range(len(par_act_sets) - 1): - # for each pair, iterate through all possible action combinations - for act_x in par_act_sets[j]: - for act_y in par_act_sets[j + 1]: + # for each action in psi_i+1 + for act_y in par_act_sets[j + 1]: + all_constraint_1 = [] + all_constraint_2 = [] + # for each action in psi_i + for act_x in par_act_sets[j]: if act_x != act_y: # calculate the probability of the actions being disordered (p) p = obs_lists.probabilities[ActionPair({act_x, act_y})] - # for each action combination, iterate through all possible propositions + # each constraint only needs to hold for one proposition to be true + constraint_1 = [] + constraint_2 = [] for r in obs_lists.propositions: - # enforce the following constraint if the actions are ordered with weight (1 - p) x wmax: - AMDN._extract_aux_set_weights(Or([ + constraint_1.append(Or([ And([pre(r, act_x), ~delete(r, act_x), delete(r, act_y)]), And([add(r, act_x), pre(r, act_y)]), And([add(r, act_x), delete(r, act_y)]), And([delete(r, act_x), add(r, act_y)]) - ]).to_CNF(), disorder_constraints, (1 - p)) - - # likewise, enforce the following constraint if the actions are disordered with weight p x wmax: - AMDN._extract_aux_set_weights(Or([ + ]).to_CNF()) + constraint_2.append(Or([ And([pre(r, act_y), ~delete(r, act_y), delete(r, act_x)]), And([add(r, act_y), pre(r, act_x)]), And([add(r, act_y), delete(r, act_x)]), And([delete(r, act_y), add(r, act_x)]) - ]).to_CNF(), disorder_constraints, p) + ]).to_CNF()) + all_constraint_1.append(Or(constraint_1).to_CNF()) + all_constraint_2.append(Or(constraint_2).to_CNF()) + # each constraint only needs to hold for one action in psi_i to be true + AMDN._extract_aux_set_weights(Or(all_constraint_1).to_CNF(), disorder_constraints, (1 - p)) + AMDN._extract_aux_set_weights(Or(all_constraint_2).to_CNF(), disorder_constraints, p) return disorder_constraints @staticmethod diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index b72854e9..61a28b7e 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -142,6 +142,7 @@ def __init__(self, traces: TraceList, Token: Type[Observation], features: List[C self.all_states = [] self.features = features self.learned_theta = learned_theta + # TODO: use functions here instead? actions = {step.action for trace in traces for step in trace if step.action} # cast to list for iteration purposes self.actions = list(actions) From a14ca5455498eeb43022c22ccedc7890b2a019f9 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 24 Aug 2021 12:09:08 -0400 Subject: [PATCH 129/181] Restore test_slaf dir --- tests/extract/test_slaf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/extract/test_slaf.py b/tests/extract/test_slaf.py index e7475faa..5bbbc08f 100644 --- a/tests/extract/test_slaf.py +++ b/tests/extract/test_slaf.py @@ -23,10 +23,10 @@ def test_slaf(): # exit out to the base macq folder so we can get to /tests base = Path(__file__).parent.parent model_blocks_dom = str( - (base / "pddl_testing_files/model_blocks_domain.pddl").resolve() + (base / "generated_testing_files/model_blocks_domain.pddl").resolve() ) model_blocks_prob = str( - (base / "pddl_testing_files/model_blocks_problem.pddl").resolve() + (base / "generated_testing_files/model_blocks_problem.pddl").resolve() ) traces = generate_blocks_traces(plan_len=2, num_traces=1) From 44ac0c3785042f1dfa71fb633c077ab30697ca68 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 24 Aug 2021 12:30:25 -0400 Subject: [PATCH 130/181] Update README.md --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0bee1b8a..33b212c1 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This library is a collection of tools for planning-like action model acquisition ## Usage ```python from macq import generate, extract -from macq.observation import IdentityObservation +from macq.observation import IdentityObservation, AtomicPartialObservation # get a domain-specific generator: uses api.planning.domains problem_id/ # generate 100 traces of length 20 using vanilla sampling @@ -66,11 +66,12 @@ Model: clear location pos-04-04 at player player-01 location pos-04-06 ... + ###################################################################### # Model Extraction - SLAF Technique ###################################################################### traces = generate.pddl.VanillaSampling(problem_id = 123, plan_len = 2, num_traces = 1).traces -observations = traces.tokenize(PartialObservabilityToken, method=PartialObservabilityToken.random_subset, percent_missing=0.10) +observations = traces.tokenize(AtomicPartialObservation, percent_missing=0.10) model = Extract(observations, modes.SLAF) model.details() @@ -92,9 +93,9 @@ Model: - [x] [Learning Planning Operators by Observation and Practice](https://aaai.org/Papers/AIPS/1994/AIPS94-057.pdf) (AIPS'94) - [ ] [Learning by Experimentation: Incremental Refinement of Incomplete Planning Domains](https://www.sciencedirect.com/science/article/pii/B9781558603356500192) (ICML'94) - [ ] [Learning Probabilistic Relational Planning Rules](https://people.csail.mit.edu/lpk/papers/2005/zpk-aaai05.pdf) (ICAPS'04) -- [ ] [Learning Action Models from Plan Examples with Incomplete Knowledge](https://www.aaai.org/Papers/ICAPS/2005/ICAPS05-025.pdf) (ICAPS'05) +- [x] [Learning Action Models from Plan Examples with Incomplete Knowledge](https://www.aaai.org/Papers/ICAPS/2005/ICAPS05-025.pdf) (ICAPS'05) - [ ] [Learning Planning Rules in Noisy Stochastic Worlds](https://people.csail.mit.edu/lpk/papers/2005/zpk-aaai05.pdf) (AAAI'05) -- [ ] [Learning action models from plan examples using weighted MAX-SAT](https://www.sciencedirect.com/science/article/pii/S0004370206001408) (AIJ'07) +- [x] [Learning action models from plan examples using weighted MAX-SAT](https://www.sciencedirect.com/science/article/pii/S0004370206001408) (AIJ'07) - [ ] [Learning Symbolic Models of Stochastic Domains](https://www.aaai.org/Papers/JAIR/Vol29/JAIR-2910.pdf) (JAIR'07) - [x] [Learning Partially Observable Deterministic Action Models](https://www.aaai.org/Papers/JAIR/Vol33/JAIR-3310.pdf) (JAIR'08) - [ ] [Acquisition of Object-Centred Domain Models from Planning Examples](https://ojs.aaai.org/index.php/ICAPS/article/view/13391) (ICAPS'09) From 86aa67cc457d7cb0e45691ca4877c767307e70ba Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 24 Aug 2021 13:44:58 -0400 Subject: [PATCH 131/181] Remove redundant code --- macq/extract/arms.py | 1 - 1 file changed, 1 deletion(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index acea139d..ce1b4317 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -24,7 +24,6 @@ def var(self): return f"{self.name} {' '.join(list(self.types))}" def matches(self, action: LearnedAction): - match = False action_types = set(action.obj_params) self_counts = Counter(self.types) action_counts = Counter(action.obj_params) From c114bed4fa7d72c8032f654f629113adf6fdd49e Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 24 Aug 2021 14:34:24 -0400 Subject: [PATCH 132/181] refactor constraints to set weights appropriately --- macq/extract/amdn.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 886f0a97..119397c0 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -91,8 +91,6 @@ def _build_disorder_constraints(obs_lists: ObservationLists): for j in range(len(par_act_sets) - 1): # for each action in psi_i+1 for act_y in par_act_sets[j + 1]: - all_constraint_1 = [] - all_constraint_2 = [] # for each action in psi_i for act_x in par_act_sets[j]: if act_x != act_y: @@ -114,11 +112,8 @@ def _build_disorder_constraints(obs_lists: ObservationLists): And([add(r, act_y), delete(r, act_x)]), And([delete(r, act_y), add(r, act_x)]) ]).to_CNF()) - all_constraint_1.append(Or(constraint_1).to_CNF()) - all_constraint_2.append(Or(constraint_2).to_CNF()) - # each constraint only needs to hold for one action in psi_i to be true - AMDN._extract_aux_set_weights(Or(all_constraint_1).to_CNF(), disorder_constraints, (1 - p)) - AMDN._extract_aux_set_weights(Or(all_constraint_2).to_CNF(), disorder_constraints, p) + AMDN._extract_aux_set_weights(Or(constraint_1).to_CNF(), disorder_constraints, (1 - p)) + AMDN._extract_aux_set_weights(Or(constraint_2).to_CNF(), disorder_constraints, p) return disorder_constraints @staticmethod From dfb7f90c17bd14dfec99f0c6821344e72979bbe9 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 24 Aug 2021 14:49:00 -0400 Subject: [PATCH 133/181] fix soft parallel constraints --- macq/extract/amdn.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 119397c0..2b614397 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -138,14 +138,11 @@ def _build_soft_parallel_constraints(obs_lists: ObservationLists): # within each parallel action set, iterate through the same action set again to compare # each action to every other action in the set; setting constraints assuming actions are not disordered for act_x in par_act_sets[j]: - for act_x_prime in par_act_sets[j]: - if act_x != act_x_prime: - p = obs_lists.probabilities[ActionPair({act_x, act_x_prime})] - # iterate through all propositions - for r in obs_lists.propositions: - # equivalent: if r is in the add or delete list of an action in the set, that implies it - # can't be in the add or delete list of any other action in the set - AMDN._extract_aux_set_weights(Or([And([~add(r, act_x_prime), ~delete(r, act_x_prime)]), And([~add(r, act_x), ~delete(r, act_x)])]).to_CNF(), soft_constraints, (1 - p)) + for act_x_prime in par_act_sets[j] - {act_x}: + p = obs_lists.probabilities[ActionPair({act_x, act_x_prime})] + # iterate through all propositions + for r in obs_lists.propositions: + soft_constraints[implies(add(r, act_x), ~delete(r, act_x_prime))] = (1 - p) * WMAX # iterate through all traces for i in range(len(obs_lists.all_par_act_sets)): @@ -154,12 +151,11 @@ def _build_soft_parallel_constraints(obs_lists: ObservationLists): for j in range(len(par_act_sets) - 1): # for each pair, compare every action in act_y to every action in act_x_prime; setting constraints assuming actions are disordered for act_y in par_act_sets[j + 1]: - for act_x_prime in par_act_sets[j]: - if act_y != act_x_prime: - p = obs_lists.probabilities[ActionPair({act_y, act_x_prime})] - # iterate through all propositions and similarly set the constraint - for r in obs_lists.propositions: - AMDN._extract_aux_set_weights(Or([And([~add(r, act_x_prime), ~delete(r, act_x_prime)]), And([~add(r, act_y), ~delete(r, act_y)])]).to_CNF(), soft_constraints, p) + for act_x_prime in par_act_sets[j] - {act_y}: + p = obs_lists.probabilities[ActionPair({act_y, act_x_prime})] + # iterate through all propositions and similarly set the constraint + for r in obs_lists.propositions: + soft_constraints[implies(add(r, act_y), ~delete(r, act_x_prime))] = p * WMAX return soft_constraints @staticmethod From 1355ab2bfc44cfb94824dc1e94b33e2df7d1ef8d Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 24 Aug 2021 14:59:58 -0400 Subject: [PATCH 134/181] document amdn paper issues --- macq/extract/amdn.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 2b614397..26785c49 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -92,6 +92,9 @@ def _build_disorder_constraints(obs_lists: ObservationLists): # for each action in psi_i+1 for act_y in par_act_sets[j + 1]: # for each action in psi_i + # NOTE: we do not use an existential here, as the paper describes (for each act_y in psi_i + 1, + # there exists an act_x in psi_i such that the condition holds.) + # this is due to the fact that the weights must be set for each action pair. for act_x in par_act_sets[j]: if act_x != act_y: # calculate the probability of the actions being disordered (p) @@ -130,6 +133,11 @@ def _build_hard_parallel_constraints(obs_lists: ObservationLists): @staticmethod def _build_soft_parallel_constraints(obs_lists: ObservationLists): soft_constraints = {} + + # NOTE: the paper does not take into account possible conflicts between the preconditions of actions + # and the add/delete effects of other actions (similar to the hard constraints, but with other actions + # in the parallel action set). + # iterate through all traces for i in range(len(obs_lists.all_par_act_sets)): par_act_sets = obs_lists.all_par_act_sets[i] From 85b22a10249798389396751cecfc88d6238cbe5b Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 24 Aug 2021 15:28:07 -0400 Subject: [PATCH 135/181] add dom/prob, update gen to handle atomic actions --- macq/generate/pddl/generator.py | 9 ++++++--- tests/extract/test_amdn.py | 13 ++++++++----- tests/pddl_testing_files/door_dom.pddl | 20 ++++++++++++++++++++ tests/pddl_testing_files/door_prob.pddl | 4 ++++ 4 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 tests/pddl_testing_files/door_dom.pddl create mode 100644 tests/pddl_testing_files/door_prob.pddl diff --git a/macq/generate/pddl/generator.py b/macq/generate/pddl/generator.py index 95250395..f4556f38 100644 --- a/macq/generate/pddl/generator.py +++ b/macq/generate/pddl/generator.py @@ -146,8 +146,12 @@ def __get_op_dict(self): """ op_dict = {} for o in self.instance.operators: - # reformat so that operators can be referenced by the same string format the planner uses for actions - op_dict["".join(["(", o.name.replace("(", " ").replace(",", "")])] = o + # special case for actions that don't take parameters + if "()" in o.name: + op_dict["".join(["(", o.name[:-2], ")"])] = o + else: + # reformat so that operators can be referenced by the same string format the planner uses for actions + op_dict["".join(["(", o.name.replace("(", " ").replace(",", "")])] = o return op_dict def __get_all_grounded_fluents(self): @@ -376,7 +380,6 @@ def generate_plan(self, from_ipc_file:bool=False, filename:str=None): else: f = open(filename, "r") plan = list(filter(lambda x: ';' not in x, f.read().splitlines())) - # convert to a list of tarski PlainOperators (actions) return Plan([self.op_dict[p] for p in plan if p in self.op_dict]) diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index a7bdd5df..d1b02fa1 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -2,7 +2,7 @@ from macq.utils.tokenization_errors import TokenizationError from tests.utils.generators import generate_blocks_traces from macq.extract import Extract, modes -from macq.generate.pddl import RandomGoalSampling, Generator +from macq.generate.pddl import RandomGoalSampling, Generator, TraceFromGoal from macq.observation import * from macq.trace import * from pathlib import Path @@ -161,12 +161,10 @@ def test_tracelist(): if __name__ == "__main__": # exit out to the base macq folder so we can get to /tests base = Path(__file__).parent.parent + """ dom = str((base / "pddl_testing_files/blocks_domain.pddl").resolve()) prob = str((base / "pddl_testing_files/blocks_problem.pddl").resolve()) - traces = test_tracelist() - - """ # TODO: replace with a domain-specific random trace generator traces = RandomGoalSampling( # prob=prob, @@ -179,9 +177,14 @@ def test_tracelist(): enforced_hill_climbing_sampling=True ).traces traces.print() + + traces = test_tracelist() """ - + dom = str((base / "pddl_testing_files/door_dom.pddl").resolve()) + prob = str((base / "pddl_testing_files/door_prob.pddl").resolve()) + traces = TraceFromGoal(dom=dom, prob=prob, observe_pres_effs=True).trace + features = [objects_shared_feature, num_parameters_feature] learned_theta = default_theta_vec(2) observations = traces.tokenize( diff --git a/tests/pddl_testing_files/door_dom.pddl b/tests/pddl_testing_files/door_dom.pddl new file mode 100644 index 00000000..9a14e3b7 --- /dev/null +++ b/tests/pddl_testing_files/door_dom.pddl @@ -0,0 +1,20 @@ +(define (domain door) + + (:requirements :strips ) + + (:predicates + (roomA ) (roomB ) (open ) + ) + + (:action open + :parameters ( ) + :precondition (and (roomA )) + :effect (and (open )) + ) + + (:action walk + :parameters ( ) + :precondition (and (roomA ) (open )) + :effect (and (not (roomA )) (roomB )) + ) +) \ No newline at end of file diff --git a/tests/pddl_testing_files/door_prob.pddl b/tests/pddl_testing_files/door_prob.pddl new file mode 100644 index 00000000..fabd0d89 --- /dev/null +++ b/tests/pddl_testing_files/door_prob.pddl @@ -0,0 +1,4 @@ +(define (problem doors) + (:domain door) + (:init (roomA )) + (:goal (and (roomB )))) \ No newline at end of file From 624c9780f4c1f256cb204ffa529ee01355699e7d Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 24 Aug 2021 16:14:27 -0400 Subject: [PATCH 136/181] ObservationLists inherits from MutableSequence, moved to observation API --- macq/extract/amdn.py | 14 +- macq/extract/extract.py | 3 +- macq/extract/observer.py | 8 +- macq/extract/slaf.py | 9 +- macq/observation/__init__.py | 8 +- macq/observation/observation.py | 34 +- macq/observation/observation_lists.py | 291 ++++++++++++++++++ macq/trace/__init__.py | 8 +- ...ered_parallel_actions_observation_lists.py | 75 +++-- macq/trace/observation_lists.py | 63 ---- macq/trace/trace_list.py | 22 +- tests/test_readme.py | 4 +- 12 files changed, 420 insertions(+), 119 deletions(-) create mode 100644 macq/observation/observation_lists.py delete mode 100644 macq/trace/observation_lists.py diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 3981f357..f13fe8ac 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -1,7 +1,7 @@ import macq.extract as extract from .model import Model -from ..trace import ObservationLists -from ..observation import NoisyPartialDisorderedParallelObservation +from ..observation import NoisyPartialDisorderedParallelObservation, ObservationLists + class AMDN: def __new__(cls, obs_lists: ObservationLists): @@ -23,7 +23,7 @@ def __new__(cls, obs_lists: ObservationLists): # (this will make it easy and efficient to refer to later, and prevents unnecessary recalculations). store as attribute # also create a list of all tuples, store as attribute - #return Model(fluents, actions) + # return Model(fluents, actions) def _build_disorder_constraints(self): # TODO: @@ -66,12 +66,12 @@ def _build_noise_constraints(self): # iterate through all tuples # for each tuple: iterate through each step over ALL the plan traces # count the number of occurrences; if higher than the user-provided parameter delta, - # store this tuple as a dictionary entry in a list of dictionaries (something like + # store this tuple as a dictionary entry in a list of dictionaries (something like # [{"action and proposition": , "occurrences of r:" 5}]). # after all iterations are through, iterate through all the tuples in this dictionary, # and set [constraint 6] with the calculated weight. # TODO: Ask - what "occurrences of all propositions" refers to - is it the total number of steps...? - + # store the initial state s0 # iterate through every step in the plan trace # at each step, check all the propositions r in the current state @@ -80,7 +80,7 @@ def _build_noise_constraints(self): # continuing the process with different propositions? Do we still count the occurrences of each proposition through # the entire trace to use when we calculate the weight? - # [constraint 8] is almost identical to [constraint 6]. Watch the order of the tuples. + # [constraint 8] is almost identical to [constraint 6]. Watch the order of the tuples. pass @@ -92,4 +92,4 @@ def _solve_constraints(self): def _convert_to_model(self): # TODO: # convert the result to a Model - pass \ No newline at end of file + pass diff --git a/macq/extract/extract.py b/macq/extract/extract.py index ea2e84a4..70e1e1d7 100644 --- a/macq/extract/extract.py +++ b/macq/extract/extract.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from enum import Enum, auto -from ..trace import ObservationLists, Action, State +from ..trace import Action, State +from ..observation import ObservationLists from .model import Model from .observer import Observer from .slaf import SLAF diff --git a/macq/extract/observer.py b/macq/extract/observer.py index e93d0bdd..c0aff35f 100644 --- a/macq/extract/observer.py +++ b/macq/extract/observer.py @@ -5,8 +5,7 @@ import macq.extract as extract from .model import Model from .learned_fluent import LearnedFluent -from ..trace import ObservationLists -from ..observation import IdentityObservation +from ..observation import IdentityObservation, ObservationLists @dataclass @@ -54,7 +53,10 @@ def _get_fluents(obs_lists: ObservationLists): for obs_list in obs_lists: for obs in obs_list: # Update fluents with the fluents in this observation - fluents.update(LearnedFluent(f.name, [o.details() for o in f.objects]) for f in obs.state.keys()) + fluents.update( + LearnedFluent(f.name, [o.details() for o in f.objects]) + for f in obs.state.keys() + ) return fluents @staticmethod diff --git a/macq/extract/slaf.py b/macq/extract/slaf.py index ada95794..ec141c63 100644 --- a/macq/extract/slaf.py +++ b/macq/extract/slaf.py @@ -4,8 +4,7 @@ from bauhaus import Encoding from .model import Model from .learned_fluent import LearnedFluent -from ..observation import AtomicPartialObservation -from ..trace import ObservationLists +from ..observation import AtomicPartialObservation, ObservationLists # only used for pretty printing in debug mode e = Encoding() @@ -167,7 +166,9 @@ def __sort_results(observations: ObservationLists, entailed: Set): # iterate through each step for o in observations: for token in o: - model_fluents.update([LearnedFluent(name=f, objects=[]) for f in token.state]) + model_fluents.update( + [LearnedFluent(name=f, objects=[]) for f in token.state] + ) # if an action was taken on this step if token.action: # set up a base LearnedAction with the known information @@ -356,7 +357,7 @@ def __as_strips_slaf(o_list: ObservationLists): phi["pos expl"] = set() phi["neg expl"] = set() - """Steps 1 (a-c) - Update every fluent in the fluent-factored transition belief formula + """Steps 1 (a-c) - Update every fluent in the fluent-factored transition belief formula with information from the last step.""" """Step 1 (a) - update the neutral effects.""" diff --git a/macq/observation/__init__.py b/macq/observation/__init__.py index 0ec84398..a1dc1c99 100644 --- a/macq/observation/__init__.py +++ b/macq/observation/__init__.py @@ -1,19 +1,23 @@ from .observation import Observation, InvalidQueryParameter +from .observation_lists import ObservationLists from .identity_observation import IdentityObservation from .partial_observation import PartialObservation from .atomic_partial_observation import AtomicPartialObservation from .noisy_observation import NoisyObservation from .noisy_partial_observation import NoisyPartialObservation -from .noisy_partial_disordered_parallel_observation import NoisyPartialDisorderedParallelObservation +from .noisy_partial_disordered_parallel_observation import ( + NoisyPartialDisorderedParallelObservation, +) __all__ = [ "Observation", + "ObservationLists", "InvalidQueryParameter", "IdentityObservation", "PartialObservation", "AtomicPartialObservation", "NoisyObservation", "NoisyPartialObservation", - "NoisyPartialDisorderedParallelObservation" + "NoisyPartialDisorderedParallelObservation", ] diff --git a/macq/observation/observation.py b/macq/observation/observation.py index 8d052f93..bfcc84fa 100644 --- a/macq/observation/observation.py +++ b/macq/observation/observation.py @@ -1,5 +1,8 @@ -from logging import warning +from warnings import warn from json import dumps +from typing import Union + +from ..trace import State, Action import random from ..trace import State @@ -23,6 +26,10 @@ class Observation: The index of the associated step in the trace it is a part of. """ + index: int + state: Union[State, None] + action: Union[Action, None] + def __init__(self, **kwargs): """ Creates an Observation object, storing the step as a token, as well as its index/"place" @@ -35,7 +42,30 @@ def __init__(self, **kwargs): if "index" in kwargs.keys(): self.index = kwargs["index"] else: - warning("Creating an Observation token without an index.") + warn("Creating an Observation token without an index.") + + def __hash__(self): + string = str(self) + if string == "Observation\n": + warn("Observation has no unique information. Generating a generic hash.") + return hash(string) + + def __str__(self): + out = "Observation\n" + if self.index is not None: + out += f" Index: {str(self.index)}\n" + if self.state: + out += f" State: {str(self.state)}\n" + if self.action: + out += f" Action: {str(self.action)}\n" + + return out + + def get_details(self): + ind = str(self.index) if self.index else "-" + state = self.state.details() if self.state else "-" + action = self.action.details() if self.action else "" + return (ind, state, action) def _matches(self, *_): raise NotImplementedError() diff --git a/macq/observation/observation_lists.py b/macq/observation/observation_lists.py new file mode 100644 index 00000000..f3a49cca --- /dev/null +++ b/macq/observation/observation_lists.py @@ -0,0 +1,291 @@ +from __future__ import annotations +from collections import defaultdict +from collections.abc import MutableSequence +from warnings import warn +from typing import Callable, Dict, List, Type, Set, TYPE_CHECKING +from inspect import cleandoc +from rich.console import Console +from rich.table import Table +from rich.text import Text + +from . import Observation +from ..trace import Action, Fluent + +if TYPE_CHECKING: + from macq.trace import TraceList + + +class MissingToken(Exception): + def __init__(self, message=None): + if message is None: + message = ( + f"Cannot create ObservationLists from a TraceList without a Token." + ) + super().__init__(message) + + +class TokenTypeMismatch(Exception): + def __init__(self, token, obs_type, message=None): + if message is None: + message = ( + "Token type does not match observation tokens." + f"Token type: {token}" + f"Observation type: {obs_type}" + ) + super().__init__(message) + + +class ObservationLists(MutableSequence): + observations: List[List[Observation]] + type: Type[Observation] + + def __init__( + self, + trace_list: TraceList = None, + Token: Type[Observation] = None, + observations: List[List[Observation]] = None, + **kwargs, + ): + if trace_list is not None: + if not Token and not observations: + raise MissingToken() + + if Token: + self.type = Token + + self.observations = [] + self.tokenize(trace_list, **kwargs) + + if observations: + self.extend(observations) + # Check that the observations are of the specified token type + if self.type and type(observations[0][0]) != self.type: + raise TokenTypeMismatch(self.type, type(observations[0][0])) + # If token type was not provided, infer it from the observations + elif not self.type: + self.type = type(observations[0][0]) + + elif observations: + self.observations = observations + self.type = type(observations[0][0]) + + else: + self.observations = [] + + def __getitem__(self, key: int): + return self.observations[key] + + def __setitem__(self, key: int, value: List[Observation]): + self.observations[key] = value + + def __delitem__(self, key: int): + del self.observations[key] + + def __iter__(self): + return iter(self.observations) + + def __len__(self): + return len(self.observations) + + def insert(self, key: int, value: List[Observation]): + self.observations.insert(key, value) + + def get_actions(self) -> Set[Action]: + actions: Set[Action] = set() + for obs_list in self: + for obs in obs_list: + action = obs.action + if action is not None: + actions.add(action) + return actions + + def get_fluents(self) -> Set[Fluent]: + fluents: Set[Fluent] = set() + for obs_list in self: + for obs in obs_list: + if obs.state: + fluents.update(list(obs.state.keys())) + return fluents + + def tokenize(self, trace_list: TraceList, **kwargs): + for trace in trace_list: + tokens = trace.tokenize(self.type, **kwargs) + self.append(tokens) + + def fetch_observations(self, query: dict) -> List[Set[Observation]]: + matches: List[Set[Observation]] = [] + for i, obs_list in enumerate(self.observations): + matches.append(set()) + for obs in obs_list: + if obs.matches(query): + matches[i].add(obs) + return matches + + def fetch_observation_windows( + self, query: dict, left: int, right: int + ) -> List[List[Observation]]: + windows = [] + matches = self.fetch_observations(query) + for i, obs_set in enumerate(matches): + for obs in obs_set: + # NOTE: obs.index starts at 1 + start = obs.index - left - 1 + end = obs.index + right + windows.append(self[i][start:end]) + return windows + + def get_transitions(self, action: str) -> List[List[Observation]]: + query = {"action": action} + return self.fetch_observation_windows(query, 0, 1) + + def get_all_transitions(self) -> Dict[Action, List[List[Observation]]]: + actions = self.get_actions() + try: + return { + action: self.get_transitions(action.details()) for action in actions + } + except AttributeError: + return {action: self.get_transitions(str(action)) for action in actions} + + def print(self, view="details", filter_func=lambda _: True, wrap=None): + """Pretty prints the trace list in the specified view. + + Arguments: + view ("details" | "color"): + Specifies the view format to print in. "details" provides a + detailed summary of each step in a trace. "color" provides a + color grid, mapping fluents in a step to either red or green + corresponding to the truth value. + filter_func (function): + Optional; Used to filter which fluents are printed in the + colorgrid display. + wrap (bool): + Determines if the output is wrapped or cut off. Details defaults + to cut off (wrap=False), color defaults to wrap (wrap=True). + """ + console = Console() + + views = ["details", "color"] + if view not in views: + warn(f'Invalid view {view}. Defaulting to "details".') + view = "details" + + obs_lists = [] + if view == "details": + if wrap is None: + wrap = False + obs_lists = [self._details(obs_list, wrap=wrap) for obs_list in self] + + elif view == "color": + if wrap is None: + wrap = True + obs_lists = [ + self._colorgrid(obs_list, filter_func=filter_func, wrap=wrap) + for obs_list in self + ] + + for obs_list in obs_lists: + console.print(obs_list) + print() + + def _details(self, obs_list: List[Observation], wrap: bool): + indent = " " * 2 + # Summarize class attributes + details = Table.grid(expand=True) + details.title = "Trace" + details.add_column() + details.add_row( + cleandoc( + f""" + Attributes: + {indent}{len(obs_list)} steps + {indent}{len(self.get_fluents())} fluents + """ + ) + ) + steps = Table( + title="Steps", box=None, show_edge=False, pad_edge=False, expand=True + ) + steps.add_column("Step", justify="right", width=8) + steps.add_column( + "State", + justify="center", + overflow="ellipsis", + max_width=100, + no_wrap=(not wrap), + ) + steps.add_column("Action", overflow="ellipsis", no_wrap=(not wrap)) + + for obs in obs_list: + ind, state, action = obs.get_details() + steps.add_row(ind, state, action) + + details.add_row(steps) + + return details + + @staticmethod + def _colorgrid(obs_list: List[Observation], filter_func: Callable, wrap: bool): + colorgrid = Table( + title="Trace", box=None, show_edge=False, pad_edge=False, expand=False + ) + colorgrid.add_column("Fluent", justify="right") + colorgrid.add_column( + header=Text("Step", justify="center"), overflow="fold", no_wrap=(not wrap) + ) + colorgrid.add_row( + "", + "".join( + [ + "|" if i < len(obs_list) and (i + 1) % 5 == 0 else " " + for i in range(len(obs_list)) + ] + ), + ) + + static = ObservationLists.get_obs_static_fluents(obs_list) + fluents = list( + filter( + filter_func, + sorted( + ObservationLists.get_obs_fluents(obs_list), + key=lambda f: float("inf") if f in static else len(str(f)), + ), + ) + ) + + for fluent in fluents: + step_str = "" + for obs in obs_list: + if obs.state and obs.state[fluent]: + step_str += "[green]" + else: + step_str += "[red]" + step_str += "■" + + colorgrid.add_row(str(fluent), step_str) + + return colorgrid + + @staticmethod + def get_obs_fluents(obs_list: List[Observation]): + fluents = set() + for obs in obs_list: + if obs.state: + fluents.update(list(obs.state.keys())) + return fluents + + @staticmethod + def get_obs_static_fluents(obs_list: List[Observation]): + fstates = defaultdict(list) + for obs in obs_list: + if obs.state: + for f, v in obs.state.items(): + fstates[f].append(v) + + static = set() + for f, states in fstates.items(): + if all(states) or not any(states): + static.add(f) + + return static diff --git a/macq/trace/__init__.py b/macq/trace/__init__.py index 5d25627d..10e79276 100644 --- a/macq/trace/__init__.py +++ b/macq/trace/__init__.py @@ -5,8 +5,9 @@ from .step import Step from .trace import Trace, SAS from .trace_list import TraceList -from .observation_lists import ObservationLists -from .disordered_parallel_actions_observation_lists import DisorderedParallelActionsObservationLists +from .disordered_parallel_actions_observation_lists import ( + DisorderedParallelActionsObservationLists, +) __all__ = [ @@ -19,6 +20,5 @@ "Trace", "SAS", "TraceList", - "ObservationLists", - "DisorderedParallelActionsObservationLists" + "DisorderedParallelActionsObservationLists", ] diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index fbfa7582..d6785ed8 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -3,15 +3,17 @@ from numpy import dot from random import random from typing import Callable, Type, Set, List -from . import ObservationLists, TraceList, Step, Action -from ..observation import Observation +from . import TraceList, Step, Action +from ..observation import Observation, ObservationLists + @dataclass class ActionPair: """dataclass that allows a pair of actions to be referenced regardless of order (that is, {action1, action2} is equivalent to {action2, action1}.) """ - actions : Set[Action] + + actions: Set[Action] def tup(self): actions = list(self.actions) @@ -24,7 +26,8 @@ def __hash__(self): sum += hash(a.details()) return sum -def default_theta_vec(k : int): + +def default_theta_vec(k: int): """Generate the default theta vector to be used in the calculation that extracts the probability of actions being disordered; used to "weight" the features. @@ -35,7 +38,8 @@ def default_theta_vec(k : int): Returns: The default theta vector. """ - return [(1/k)] * k + return [(1 / k)] * k + def objects_shared_feature(act_x: Action, act_y: Action): """Corresponds to default feature 1 from the AMDN paper. @@ -51,11 +55,12 @@ def objects_shared_feature(act_x: Action, act_y: Action): """ num_shared = 0 for obj in act_x.obj_params: - for other_obj in act_y. obj_params: + for other_obj in act_y.obj_params: if obj == other_obj: num_shared += 1 return num_shared + def num_parameters_feature(act_x: Action, act_y: Action): """Corresponds to default feature 2 from the AMDN paper. @@ -70,6 +75,7 @@ def num_parameters_feature(act_x: Action, act_y: Action): """ return 1 if len(act_x.obj_params) == len(act_y.obj_params) else 0 + def _decision(probability: float): """Makes a decision based on the given probability. @@ -82,12 +88,13 @@ def _decision(probability: float): """ return random() < probability + class DisorderedParallelActionsObservationLists(ObservationLists): - """Alternate ObservationLists type that enforces appropriate actions to be disordered and/or parallel. + """Alternate ObservationLists type that enforces appropriate actions to be disordered and/or parallel. Inherits the base ObservationLists class. The default feature functions and theta vector described in the AMDN paper are available for use in this module. - + Attributes: traces (List[List[Token]]): The trace list converted to a list of lists of tokens. @@ -105,7 +112,15 @@ class DisorderedParallelActionsObservationLists(ObservationLists): A dictionary that contains a mapping of each possible `ActionPair` and the probability that the actions in them are disordered. """ - def __init__(self, traces: TraceList, Token: Type[Observation], features: List[Callable], learned_theta: List[float], **kwargs): + + def __init__( + self, + traces: TraceList, + Token: Type[Observation], + features: List[Callable], + learned_theta: List[float], + **kwargs + ): """AI is creating summary for __init__ Args: @@ -128,7 +143,11 @@ def __init__(self, traces: TraceList, Token: Type[Observation], features: List[C # cast to list for iteration purposes self.actions = list(actions) # create |A| (action x action set, no duplicates) - self.cross_actions = [ActionPair({self.actions[i], self.actions[j]}) for i in range(len(self.actions)) for j in range(i + 1, len(self.actions))] + self.cross_actions = [ + ActionPair({self.actions[i], self.actions[j]}) + for i in range(len(self.actions)) + for j in range(i + 1, len(self.actions)) + ] # dictionary that holds the probabilities of all actions being disordered self.probabilities = self._calculate_all_probabilities() self.tokenize(traces, Token, **kwargs) @@ -180,8 +199,10 @@ def _calculate_probability(self, act_x: Action, act_y: Action): numerator = self._theta_dot_features_calc(f_vec, theta_vec) denominator = 0 for combo in self.cross_actions: - denominator += self._theta_dot_features_calc(self._get_f_vec(*combo.tup()), theta_vec) - return numerator/denominator + denominator += self._theta_dot_features_calc( + self._get_f_vec(*combo.tup()), theta_vec + ) + return numerator / denominator def _calculate_all_probabilities(self): """Calculates the probabilities of all combinations of actions being disordered. @@ -209,7 +230,7 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): Any extra arguments to be supplied to the Token __init__. """ # build parallel action sets - for trace in traces: + for trace in traces: par_act_sets = [] states = [] cur_par_act = set() @@ -223,11 +244,15 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): for i in range(len(trace)): a = trace[i].action if a: - a_conditions = set([p for p in a.precond] + [e for e in a.add] + [e for e in a.delete]) + a_conditions = set( + [p for p in a.precond] + + [e for e in a.add] + + [e for e in a.delete] + ) # if the action has any conditions in common with any actions in the previous parallel set (NOT parallel) - if a_conditions.intersection(cur_par_act_conditions) != set(): + if a_conditions.intersection(cur_par_act_conditions) != set(): # add psi_k and s'_k to the final (ordered) lists of parallel action sets and states - par_act_sets.append(cur_par_act) + par_act_sets.append(cur_par_act) states.append(cur_state) # reset psi_k (that is, create a new parallel action set) cur_par_act = set() @@ -240,7 +265,7 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): cur_par_act_conditions.update(a_conditions) # if on the last step of the trace, add the current set/state to the final result before exiting the loop if i == len(trace) - 1: - par_act_sets.append(cur_par_act) + par_act_sets.append(cur_par_act) states.append(cur_state) # generate disordered actions - do trace by trace @@ -252,7 +277,9 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): for act_y in par_act_sets[j]: if act_x != act_y: # get probability and divide by distance - prob = self.probabilities[ActionPair({act_x, act_y})]/(j - i) + prob = self.probabilities[ + ActionPair({act_x, act_y}) + ] / (j - i) if _decision(prob): par_act_sets[i].discard(act_x) par_act_sets[i].add(act_y) @@ -262,6 +289,12 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): tokens = [] for i in range(len(par_act_sets)): for act in par_act_sets[i]: - tokens.append(Token(Step(state=states[i], action=act, index=i), par_act_set_ID = i, **kwargs)) - self.append(tokens) - \ No newline at end of file + tokens.append( + Token( + Step(state=states[i], action=act, index=i), + par_act_set_ID=i, + **kwargs + ) + ) + self.append(tokens) + diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py deleted file mode 100644 index fab452b5..00000000 --- a/macq/trace/observation_lists.py +++ /dev/null @@ -1,63 +0,0 @@ -import macq.trace as TraceAPI -from typing import List, Set, Type -from ..observation import Observation -from . import Trace - -class ObservationLists(TraceAPI.TraceList): - traces: List[List[Observation]] - # Disable methods - generate_more = property() - get_usage = property() - tokenize = property() - get_fluents = property() - - def __init__(self, traces: TraceAPI.TraceList, Token: Type[Observation], **kwargs): - self.traces = [] - self.type = Token - self.tokenize(traces, **kwargs) - - def tokenize(self, traces: TraceAPI.TraceList, **kwargs): - trace: Trace - for trace in traces: - tokens = trace.tokenize(self.type, **kwargs) - self.append(tokens) - - def fetch_observations(self, query: dict): - matches: List[Set[Observation]] = list() - trace: List[Observation] - for i, trace in enumerate(self): - matches.append(set()) - for obs in trace: - if obs.matches(query): # if no matches, set can be empty - matches[i].add(obs) - return matches # list of sets of matching fluents from each trace - - def fetch_observation_windows(self, query: dict, left: int, right: int): - windows = [] - matches = self.fetch_observations(query) - trace: Set[Observation] - for i, trace in enumerate(matches): # note obs.index starts at 1 (index = i+1) - for obs in trace: - start = obs.index - left - 1 - end = obs.index + right - windows.append(self[i][start:end]) - return windows - - def get_transitions(self, action: str): - query = {"action": action} - return self.fetch_observation_windows(query, 0, 1) - - def get_all_transitions(self): - actions = set() - for trace in self: - for obs in trace: - action = obs.action - if action: - actions.add(action) - # Actions in the observations can be either Action objects or strings depending on the type of observation - try: - return { - action: self.get_transitions(action.details()) for action in actions - } - except AttributeError: - return {action: self.get_transitions(str(action)) for action in actions} \ No newline at end of file diff --git a/macq/trace/trace_list.py b/macq/trace/trace_list.py index a154fdb3..40e097c1 100644 --- a/macq/trace/trace_list.py +++ b/macq/trace/trace_list.py @@ -1,10 +1,9 @@ from logging import warn from typing import List, Callable, Type, Optional, Set from rich.console import Console -from . import Action, Trace -from ..observation import Observation -import macq.trace as TraceAPI +from . import Action, Trace +from ..observation import Observation, ObservationLists class TraceList: @@ -20,8 +19,7 @@ class TraceList: The function used to generate the traces. """ - # Allow child classes to have traces as a list of any type - traces: List + traces: List[Trace] class MissingGenerator(Exception): def __init__( @@ -179,17 +177,19 @@ def get_fluents(self): fluents.update(step.state.fluents) return fluents - def tokenize(self, Token: Type[Observation], ObsLists = None, **kwargs): + def tokenize( + self, + Token: Type[Observation], + ObsLists: Type[ObservationLists] = ObservationLists, + **kwargs, + ): """Tokenizes the steps in this trace. Args: Token (Observation): A subclass of `Observation`, defining the method of tokenization for the steps. - ObsLists (Type[TraceAPI.ObservationLists]): + ObsLists (Type[ObservationLists]): The type of `ObservationLists` to be used. Defaults to the base `ObservationLists`. """ - ObsLists : Type[TraceAPI.ObservationLists] - if not ObsLists: - ObsLists = TraceAPI.ObservationLists - return ObsLists(self, Token, **kwargs) \ No newline at end of file + return ObsLists(self, Token, **kwargs) diff --git a/tests/test_readme.py b/tests/test_readme.py index 594a0e2b..217c7305 100644 --- a/tests/test_readme.py +++ b/tests/test_readme.py @@ -22,6 +22,7 @@ def test_readme(): assert len(traces) == 4 action1 = traces[0][0].action + assert action1 trace = traces[0] assert len(trace) == 5 @@ -42,4 +43,5 @@ def test_readme(): # run as a script to look over the extracted model traces = generate_traces() model = extract_model(traces) - print(model.details()) \ No newline at end of file + print(model.details()) + From 0467d339d2d800b394fc27cc7658b03369440584 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 24 Aug 2021 16:33:07 -0400 Subject: [PATCH 137/181] Check for matching token type on insert --- macq/observation/observation_lists.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/macq/observation/observation_lists.py b/macq/observation/observation_lists.py index f3a49cca..66a449b3 100644 --- a/macq/observation/observation_lists.py +++ b/macq/observation/observation_lists.py @@ -11,6 +11,7 @@ from . import Observation from ..trace import Action, Fluent +# Prevents circular importing if TYPE_CHECKING: from macq.trace import TraceList @@ -36,6 +37,17 @@ def __init__(self, token, obs_type, message=None): class ObservationLists(MutableSequence): + """A sequence of observations. + + A `list`-like object, where each element is a list of `Observation`s. + + Attributes: + observations (List[List[Observation]]): + The internal list of lists of `Observation` objects. + type (Type[Observation]): + The type (class) of the observations. + """ + observations: List[List[Observation]] type: Type[Observation] @@ -71,12 +83,17 @@ def __init__( else: self.observations = [] + self.type = Observation def __getitem__(self, key: int): return self.observations[key] def __setitem__(self, key: int, value: List[Observation]): self.observations[key] = value + if self.type == Observation: + self.type = type(value[0]) + elif type(value[0] != self.type): + raise TokenTypeMismatch(self.type, type(value[0])) def __delitem__(self, key: int): del self.observations[key] @@ -89,6 +106,10 @@ def __len__(self): def insert(self, key: int, value: List[Observation]): self.observations.insert(key, value) + if self.type == Observation: + self.type = type(value[0]) + elif type(value[0] != self.type): + raise TokenTypeMismatch(self.type, type(value[0])) def get_actions(self) -> Set[Action]: actions: Set[Action] = set() From 125be779d63e30b859ba04a8741148ce315dfd63 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 24 Aug 2021 16:34:34 -0400 Subject: [PATCH 138/181] Fix token type-check --- macq/observation/observation_lists.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macq/observation/observation_lists.py b/macq/observation/observation_lists.py index 66a449b3..228e1ae9 100644 --- a/macq/observation/observation_lists.py +++ b/macq/observation/observation_lists.py @@ -92,7 +92,7 @@ def __setitem__(self, key: int, value: List[Observation]): self.observations[key] = value if self.type == Observation: self.type = type(value[0]) - elif type(value[0] != self.type): + elif type(value[0]) != self.type: raise TokenTypeMismatch(self.type, type(value[0])) def __delitem__(self, key: int): @@ -108,7 +108,7 @@ def insert(self, key: int, value: List[Observation]): self.observations.insert(key, value) if self.type == Observation: self.type = type(value[0]) - elif type(value[0] != self.type): + elif type(value[0]) != self.type: raise TokenTypeMismatch(self.type, type(value[0])) def get_actions(self) -> Set[Action]: From d5bb6052fbf85b6b87b4772dd08d915d19611f10 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 24 Aug 2021 16:36:03 -0400 Subject: [PATCH 139/181] TraceList inherits from MutableSequence --- macq/trace/trace_list.py | 140 +++++++++++++++++---------------- tests/trace/test_trace_list.py | 1 + 2 files changed, 72 insertions(+), 69 deletions(-) diff --git a/macq/trace/trace_list.py b/macq/trace/trace_list.py index 40e097c1..b8fc4a28 100644 --- a/macq/trace/trace_list.py +++ b/macq/trace/trace_list.py @@ -1,26 +1,25 @@ from logging import warn -from typing import List, Callable, Type, Optional, Set +from collections.abc import MutableSequence +from typing import List, Callable, Type, Union from rich.console import Console from . import Action, Trace from ..observation import Observation, ObservationLists -class TraceList: - """A collection of traces. +class TraceList(MutableSequence): + """A sequence of traces. A `list`-like object, where each element is a `Trace` of the same planning problem. Attributes: - traces (list): + traces (List[Trace]): The list of `Trace` objects. - generator (function | None): + generator (Callable | None): The function used to generate the traces. """ - traces: List[Trace] - class MissingGenerator(Exception): def __init__( self, @@ -31,107 +30,76 @@ def __init__( self.message = message super().__init__(message) + traces: List[Trace] + generator: Union[Callable, None] + def __init__( self, traces: List[Trace] = None, - generator: Optional[Callable] = None, + generator: Callable = None, ): """Initializes a TraceList with a list of traces and a generator. Args: - traces (list): + traces (List[Trace]): Optional; The list of `Trace` objects. - generator (function): + generator (Callable): Optional; The function used to generate the traces. """ self.traces = [] if traces is None else traces self.generator = generator - def __len__(self): - return len(self.traces) + def __getitem__(self, key: int): + return self.traces[key] def __setitem__(self, key: int, value: Trace): self.traces[key] = value - def __getitem__(self, key: int): - return self.traces[key] - def __delitem__(self, key: int): del self.traces[key] def __iter__(self): return iter(self.traces) - def __reversed__(self): - return reversed(self.traces) + def __len__(self): + return len(self.traces) + + # def __reversed__(self): + # return reversed(self.traces) - def __contains__(self, item): - return item in self.traces + # def __contains__(self, item): + # return item in self.traces - def append(self, item): - self.traces.append(item) + # def append(self, item): + # self.traces.append(item) - def clear(self): - self.traces.clear() + # def clear(self): + # self.traces.clear() def copy(self): return self.traces.copy() - def extend(self, iterable): - self.traces.extend(iterable) + # def extend(self, iterable): + # self.traces.extend(iterable) - def index(self, value): - return self.traces.index(value) + # def index(self, value): + # return self.traces.index(value) - def insert(self, index: int, item): - self.traces.insert(index, item) + def insert(self, key: int, value: Trace): + self.traces.insert(key, value) - def pop(self): - return self.traces.pop() + # def pop(self): + # return self.traces.pop() - def remove(self, value): - self.traces.remove(value) + # def remove(self, value): + # self.traces.remove(value) - def reverse(self): - self.traces.reverse() + # def reverse(self): + # self.traces.reverse() def sort(self, reverse: bool = False, key: Callable = lambda e: e.get_total_cost()): self.traces.sort(reverse=reverse, key=key) - def print(self, view="details", filter_func=lambda _: True, wrap=None): - """Pretty prints the trace list in the specified view. - - Arguments: - view ("details" | "color"): - Specifies the view format to print in. "details" provides a - detailed summary of each step in a trace. "color" provides a - color grid, mapping fluents in a step to either red or green - corresponding to the truth value. - """ - console = Console() - - views = ["details", "color"] - if view not in views: - warn(f'Invalid view {view}. Defaulting to "details".') - view = "details" - - traces = [] - if view == "details": - if wrap is None: - wrap = False - traces = [trace.details(wrap=wrap) for trace in self] - - elif view == "color": - if wrap is None: - wrap = True - traces = [ - trace.colorgrid(filter_func=filter_func, wrap=wrap) for trace in self - ] - - for trace in traces: - console.print(trace) - print() - def generate_more(self, num: int): """Generates more traces using the generator function. @@ -193,3 +161,37 @@ def tokenize( The type of `ObservationLists` to be used. Defaults to the base `ObservationLists`. """ return ObsLists(self, Token, **kwargs) + + def print(self, view="details", filter_func=lambda _: True, wrap=None): + """Pretty prints the trace list in the specified view. + + Arguments: + view ("details" | "color"): + Specifies the view format to print in. "details" provides a + detailed summary of each step in a trace. "color" provides a + color grid, mapping fluents in a step to either red or green + corresponding to the truth value. + """ + console = Console() + + views = ["details", "color"] + if view not in views: + warn(f'Invalid view {view}. Defaulting to "details".') + view = "details" + + traces = [] + if view == "details": + if wrap is None: + wrap = False + traces = [trace.details(wrap=wrap) for trace in self] + + elif view == "color": + if wrap is None: + wrap = True + traces = [ + trace.colorgrid(filter_func=filter_func, wrap=wrap) for trace in self + ] + + for trace in traces: + console.print(trace) + print() diff --git a/tests/trace/test_trace_list.py b/tests/trace/test_trace_list.py index 200ff93f..29368cc9 100644 --- a/tests/trace/test_trace_list.py +++ b/tests/trace/test_trace_list.py @@ -22,6 +22,7 @@ def test_trace_list(): assert trace_list[0] is first action = trace_list[0].steps[0].action + assert action usages = trace_list.get_usage(action) for i, trace in enumerate(trace_list): assert usages[i] == trace.get_usage(action) From acc2cca8d2a2e31a686089854899dbba16ebe1d8 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 24 Aug 2021 16:36:36 -0400 Subject: [PATCH 140/181] Cleanup unnessecary code --- macq/trace/trace_list.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/macq/trace/trace_list.py b/macq/trace/trace_list.py index b8fc4a28..2c50722d 100644 --- a/macq/trace/trace_list.py +++ b/macq/trace/trace_list.py @@ -64,39 +64,12 @@ def __iter__(self): def __len__(self): return len(self.traces) - # def __reversed__(self): - # return reversed(self.traces) - - # def __contains__(self, item): - # return item in self.traces - - # def append(self, item): - # self.traces.append(item) - - # def clear(self): - # self.traces.clear() - def copy(self): return self.traces.copy() - # def extend(self, iterable): - # self.traces.extend(iterable) - - # def index(self, value): - # return self.traces.index(value) - def insert(self, key: int, value: Trace): self.traces.insert(key, value) - # def pop(self): - # return self.traces.pop() - - # def remove(self, value): - # self.traces.remove(value) - - # def reverse(self): - # self.traces.reverse() - def sort(self, reverse: bool = False, key: Callable = lambda e: e.get_total_cost()): self.traces.sort(reverse=reverse, key=key) From b885f4c1dac460d1022809713ce706794c81fd4e Mon Sep 17 00:00:00 2001 From: beckydvn Date: Wed, 25 Aug 2021 14:23:36 -0400 Subject: [PATCH 141/181] write debug mode for dc constr and replace noisy f --- macq/extract/amdn.py | 129 ++++++++++++------ macq/observation/noisy_observation.py | 21 ++- ...partial_disordered_parallel_observation.py | 7 +- macq/observation/noisy_partial_observation.py | 7 +- tests/extract/test_amdn.py | 9 +- 5 files changed, 122 insertions(+), 51 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 26785c49..51f2c78d 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -5,38 +5,46 @@ from nnf import Aux, Var, And, Or from pysat.formula import WCNF from pysat.examples.rc2 import RC2 +from bauhaus import Encoding # only used for pretty printing in debug mode from .exceptions import ( IncompatibleObservationToken, InvalidMaxSATModel, ) from .model import Model -from ..trace import ObservationLists, ActionPair, Fluent # for typing +from ..trace import ObservationLists, ActionPair from ..observation import NoisyPartialDisorderedParallelObservation -from ..utils.pysat import to_wcnf, encode +from ..utils.pysat import to_wcnf -def __set_precond(r, act): - return Var(str(r)[1:-1] + " is a precondition of " + act.details()) +e = Encoding -def __set_add(r, act): - return Var(str(r)[1:-1] + " is added by " + act.details()) +def _set_precond(r, act): + return Var("(" + str(r)[1:-1] + " is a precondition of " + act.details() + ")") -def __set_del(r, act): - return Var(str(r)[1:-1] + " is deleted by " + act.details()) +def _set_add(r, act): + return Var("(" + str(r)[1:-1] + " is added by " + act.details() + ")") + +def _set_del(r, act): + return Var("(" + str(r)[1:-1] + " is deleted by " + act.details() + ")") # for easier reference -pre = __set_precond -add = __set_add -delete = __set_del +pre = _set_precond +add = _set_add +delete = _set_del WMAX = 1 class AMDN: - def __new__(cls, obs_lists: ObservationLists, occ_threshold: int): + def __new__(cls, obs_lists: ObservationLists, debug: bool = False, occ_threshold: int = 1): """Creates a new Model object. Args: obs_lists (ObservationList): The state observations to extract the model from. + debug (bool): + Optional debugging mode. + occ_threshold (int): + Threshold to be used for noise constraints. + Raises: IncompatibleObservationToken: Raised if the observations are not identity observation. @@ -44,11 +52,11 @@ def __new__(cls, obs_lists: ObservationLists, occ_threshold: int): if obs_lists.type is not NoisyPartialDisorderedParallelObservation: raise IncompatibleObservationToken(obs_lists.type, AMDN) - return AMDN._amdn(obs_lists, occ_threshold) + return AMDN._amdn(obs_lists, debug, occ_threshold) @staticmethod - def _amdn(obs_lists: ObservationLists, occ_threshold: int): - wcnf, decode = AMDN._solve_constraints(obs_lists, occ_threshold) + def _amdn(obs_lists: ObservationLists, debug: int, occ_threshold: int): + wcnf, decode = AMDN._solve_constraints(obs_lists, debug, occ_threshold) raw_model = AMDN._extract_raw_model(wcnf, decode) return AMDN._extract_model(obs_lists, raw_model) @@ -67,20 +75,28 @@ def _or_refactor(maybe_lit: Union[Or, Var]): @staticmethod def _extract_aux_set_weights(cnf_formula: And[Or[Var]], constraints: Dict, prob_disordered: float): - aux_var = set() # find all the auxiliary variables for clause in cnf_formula.children: for var in clause.children: if isinstance(var.name, Aux): - aux_var.add(var.name) + # only have the case where the clause is part of an aux <-> formula if the other variables are not aux + valid = True + for other in clause.children - {var}: + if isinstance(other.name, Aux): + valid = False + break + if valid: + # aux variables are the soft clauses that get the original weight + constraints[AMDN._or_refactor(var)] = prob_disordered * WMAX + else: + break # set each original constraint to be a hard clause constraints[clause] = "HARD" - # aux variables are the soft clauses that get the original weight - for aux in aux_var: - constraints[AMDN._or_refactor(Var(aux))] = prob_disordered * WMAX @staticmethod - def _build_disorder_constraints(obs_lists: ObservationLists): + def _build_disorder_constraints(obs_lists: ObservationLists, debug: int): + global e + disorder_constraints = {} # iterate through all traces for i in range(len(obs_lists.all_par_act_sets)): @@ -108,19 +124,56 @@ def _build_disorder_constraints(obs_lists: ObservationLists): And([add(r, act_x), pre(r, act_y)]), And([add(r, act_x), delete(r, act_y)]), And([delete(r, act_x), add(r, act_y)]) - ]).to_CNF()) + ])) constraint_2.append(Or([ And([pre(r, act_y), ~delete(r, act_y), delete(r, act_x)]), And([add(r, act_y), pre(r, act_x)]), And([add(r, act_y), delete(r, act_x)]), And([delete(r, act_y), add(r, act_x)]) - ]).to_CNF()) - AMDN._extract_aux_set_weights(Or(constraint_1).to_CNF(), disorder_constraints, (1 - p)) - AMDN._extract_aux_set_weights(Or(constraint_2).to_CNF(), disorder_constraints, p) + ])) + disjunct_all_constr_1 = Or(constraint_1).to_CNF() + disjunct_all_constr_2 = Or(constraint_2).to_CNF() + AMDN._extract_aux_set_weights(disjunct_all_constr_1, disorder_constraints, (1 - p)) + AMDN._extract_aux_set_weights(disjunct_all_constr_2, disorder_constraints, p) + + if debug: + aux_map = {} + print("\nFor the pair: " + act_x.details() + " and " + act_y.details() + ":") + print("DC:\n") + index = 0 + for c in disorder_constraints: + for var in c.children: + if isinstance(var.name, Aux) and var.name not in aux_map: + aux_map[var.name] = f"aux {index}" + index += 1 + + all_pretty_c = {} + for c in disorder_constraints: + pretty_c = [] + for var in c.children: + if isinstance(var.name, Aux): + if var.true: + pretty_c.append(Var(aux_map[var.name])) + else: + pretty_c.append(~Var(aux_map[var.name])) + else: + pretty_c.append(var) + # map disorder constraints to pretty disorder constraints + all_pretty_c[c] = Or(pretty_c) + + for aux in aux_map.values(): + for c, v in all_pretty_c.items(): + for child in v.children: + if aux == child.name: + e.pprint(e, v) + print(disorder_constraints[c]) + break + print() + return disorder_constraints @staticmethod - def _build_hard_parallel_constraints(obs_lists: ObservationLists): + def _build_hard_parallel_constraints(obs_lists: ObservationLists, debug: int): hard_constraints = {} # create a list of all tuples for act in obs_lists.actions: @@ -131,7 +184,7 @@ def _build_hard_parallel_constraints(obs_lists: ObservationLists): return hard_constraints @staticmethod - def _build_soft_parallel_constraints(obs_lists: ObservationLists): + def _build_soft_parallel_constraints(obs_lists: ObservationLists, debug: int): soft_constraints = {} # NOTE: the paper does not take into account possible conflicts between the preconditions of actions @@ -167,8 +220,8 @@ def _build_soft_parallel_constraints(obs_lists: ObservationLists): return soft_constraints @staticmethod - def _build_parallel_constraints(obs_lists: ObservationLists): - return {**AMDN._build_hard_parallel_constraints(obs_lists), **AMDN._build_soft_parallel_constraints(obs_lists)} + def _build_parallel_constraints(obs_lists: ObservationLists, debug: int): + return {**AMDN._build_hard_parallel_constraints(obs_lists, debug), **AMDN._build_soft_parallel_constraints(obs_lists, debug)} @staticmethod def _calculate_all_r_occ(obs_lists: ObservationLists): @@ -190,7 +243,7 @@ def _set_up_occurrences_dict(obs_lists: ObservationLists): return occurrences @staticmethod - def _noise_constraints_6(obs_lists: ObservationLists, all_occ: int, occ_threshold: int): + def _noise_constraints_6(obs_lists: ObservationLists, debug: int, all_occ: int, occ_threshold: int): noise_constraints_6 = {} occurrences = AMDN._set_up_occurrences_dict(obs_lists) @@ -215,7 +268,7 @@ def _noise_constraints_6(obs_lists: ObservationLists, all_occ: int, occ_threshol return noise_constraints_6 @staticmethod - def _noise_constraints_7(obs_lists: ObservationLists, all_occ: int): + def _noise_constraints_7(obs_lists: ObservationLists, debug: int, all_occ: int): noise_constraints_7 = {} # set up dict occurrences = {} @@ -244,7 +297,7 @@ def _noise_constraints_7(obs_lists: ObservationLists, all_occ: int): return noise_constraints_7 @staticmethod - def _noise_constraints_8(obs_lists, all_occ: int, occ_threshold: int): + def _noise_constraints_8(obs_lists, all_occ: int, debug: int, occ_threshold: int): noise_constraints_8 = {} occurrences = AMDN._set_up_occurrences_dict(obs_lists) @@ -271,18 +324,18 @@ def _noise_constraints_8(obs_lists, all_occ: int, occ_threshold: int): return noise_constraints_8 @staticmethod - def _build_noise_constraints(obs_lists: ObservationLists, occ_threshold: int): + def _build_noise_constraints(obs_lists: ObservationLists, debug: int, occ_threshold: int): # calculate all occurrences for use in weights all_occ = AMDN._calculate_all_r_occ(obs_lists) - return{**AMDN._noise_constraints_6(obs_lists, all_occ, occ_threshold), **AMDN._noise_constraints_7(obs_lists, all_occ), **AMDN._noise_constraints_8(obs_lists, all_occ, occ_threshold)} + return{**AMDN._noise_constraints_6(obs_lists, debug, all_occ, occ_threshold), **AMDN._noise_constraints_7(obs_lists, debug, all_occ), **AMDN._noise_constraints_8(obs_lists, debug, all_occ, occ_threshold)} @staticmethod - def _set_all_constraints(obs_lists: ObservationLists, occ_threshold: int): - return {**AMDN._build_disorder_constraints(obs_lists), **AMDN._build_parallel_constraints(obs_lists), **AMDN._build_noise_constraints(obs_lists, occ_threshold)} + def _set_all_constraints(obs_lists: ObservationLists, debug: int, occ_threshold: int): + return {**AMDN._build_disorder_constraints(obs_lists, debug), **AMDN._build_parallel_constraints(obs_lists, debug), **AMDN._build_noise_constraints(obs_lists, debug, occ_threshold)} @staticmethod - def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int): - constraints = AMDN._set_all_constraints(obs_lists, occ_threshold) + def _solve_constraints(obs_lists: ObservationLists, debug: int, occ_threshold: int): + constraints = AMDN._set_all_constraints(obs_lists, debug, occ_threshold) # extract hard constraints hard_constraints = [] for c, weight in constraints.items(): diff --git a/macq/observation/noisy_observation.py b/macq/observation/noisy_observation.py index ab09ebdd..442c4eb2 100644 --- a/macq/observation/noisy_observation.py +++ b/macq/observation/noisy_observation.py @@ -1,3 +1,4 @@ +import random from . import Observation from ..trace import Step from ..utils import PercentError#, extract_fluent_subset @@ -13,7 +14,7 @@ class NoisyObservation(Observation): """ def __init__( - self, step: Step, percent_noisy: float = 0): + self, step: Step, percent_noisy: float = 0, replace: bool = False): """ Creates an NoisyObservation object, storing the state and action. @@ -22,6 +23,9 @@ def __init__( The step associated with this observation. percent_noisy (float): The percentage of fluents to randomly make noisy in the observation. + replace (bool): + Option to replace noisy fluents with the values of other existing fluents instead + of just flipping their values. """ super().__init__(index=step.index) @@ -29,12 +33,12 @@ def __init__( if percent_noisy > 1 or percent_noisy < 0: raise PercentError() - step = self.random_noisy_subset(step, percent_noisy) + step = self.random_noisy_subset(step, percent_noisy, replace) self.state = step.state.clone() self.action = None if step.action is None else step.action.clone() - def random_noisy_subset(self, step: Step, percent_noisy: float): + def random_noisy_subset(self, step: Step, percent_noisy: float, replace: bool = False): """Generates a random subset of fluents corresponding to the percent provided and flips their value to create noise. @@ -43,6 +47,9 @@ def random_noisy_subset(self, step: Step, percent_noisy: float): The step associated with this observation. percent_noisy (float): The percentage of fluents to randomly make noisy in the observation. + replace (bool): + Option to replace noisy fluents with the values of other existing fluents instead + of just flipping their values. Returns: A new `Step` with the noisy fluents in place. @@ -54,8 +61,12 @@ def random_noisy_subset(self, step: Step, percent_noisy: float): for f in invisible_f: del visible_f[f] noisy_f = self.extract_fluent_subset(visible_f, percent_noisy) - for f in state: - state[f] = not state[f] if f in noisy_f else state[f] + if not replace: + for f in state: + state[f] = not state[f] if f in noisy_f else state[f] + else: + for f in state: + state[f] = state[random.choice(list(visible_f.keys()))] if f in noisy_f else state[f] return Step(state, step.action, step.index) diff --git a/macq/observation/noisy_partial_disordered_parallel_observation.py b/macq/observation/noisy_partial_disordered_parallel_observation.py index d5d22943..48821f36 100644 --- a/macq/observation/noisy_partial_disordered_parallel_observation.py +++ b/macq/observation/noisy_partial_disordered_parallel_observation.py @@ -10,7 +10,7 @@ class NoisyPartialDisorderedParallelObservation(NoisyPartialObservation): disordered with an action in another parallel action set. Finally, a "parallel action set ID" is stored which indicates which parallel action set the token is a part of. Inherits the NoisyPartialObservation token class. """ - def __init__(self, step: Step, par_act_set_ID: int, percent_missing: float = 0, hide: Set[Fluent] = None, percent_noisy: float = 0): + def __init__(self, step: Step, par_act_set_ID: int, percent_missing: float = 0, hide: Set[Fluent] = None, percent_noisy: float = 0, replace: bool = False): """ Creates an NoisyPartialDisorderedParallelObservation object. @@ -25,6 +25,9 @@ def __init__(self, step: Step, par_act_set_ID: int, percent_missing: float = 0, The set of fluents to explicitly hide in the observation. percent_noisy (float): The percentage of fluents to randomly make noisy in the observation. + replace (bool): + Option to replace noisy fluents with the values of other existing fluents instead + of just flipping their values. """ - super().__init__(step=step, percent_missing=percent_missing, hide=hide, percent_noisy=percent_noisy) + super().__init__(step=step, percent_missing=percent_missing, hide=hide, percent_noisy=percent_noisy, replace=replace) self.par_act_set_ID = par_act_set_ID diff --git a/macq/observation/noisy_partial_observation.py b/macq/observation/noisy_partial_observation.py index 0b82e025..f3056cd3 100644 --- a/macq/observation/noisy_partial_observation.py +++ b/macq/observation/noisy_partial_observation.py @@ -13,7 +13,7 @@ class NoisyPartialObservation(PartialObservation, NoisyObservation): """ def __init__( - self, step: Step, percent_missing: float = 0, hide: Set[Fluent] = None, percent_noisy: float = 0): + self, step: Step, percent_missing: float = 0, hide: Set[Fluent] = None, percent_noisy: float = 0, replace: bool = False): """ Creates an NoisyPartialObservation object. @@ -26,8 +26,11 @@ def __init__( The set of fluents to explicitly hide in the observation. percent_noisy (float): The percentage of fluents to randomly make noisy in the observation. + replace (bool): + Option to replace noisy fluents with the values of other existing fluents instead + of just flipping their values. """ # get state and action with missing fluents (updates self.state and self.action) PartialObservation.__init__(self, step=step, percent_missing=percent_missing, hide=hide) # get state and action with noisy fluents, using the updated state and action (then updates self.state and self.action) - NoisyObservation.__init__(self, step=Step(self.state, self.action, step.index), percent_noisy=percent_noisy) \ No newline at end of file + NoisyObservation.__init__(self, step=Step(self.state, self.action, step.index), percent_noisy=percent_noisy, replace=replace) \ No newline at end of file diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index d1b02fa1..b6b67a5b 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -183,7 +183,7 @@ def test_tracelist(): dom = str((base / "pddl_testing_files/door_dom.pddl").resolve()) prob = str((base / "pddl_testing_files/door_prob.pddl").resolve()) - traces = TraceFromGoal(dom=dom, prob=prob, observe_pres_effs=True).trace + traces = TraceList([TraceFromGoal(dom=dom, prob=prob, observe_pres_effs=True).trace]) features = [objects_shared_feature, num_parameters_feature] learned_theta = default_theta_vec(2) @@ -192,9 +192,10 @@ def test_tracelist(): ObsLists=DisorderedParallelActionsObservationLists, features=features, learned_theta=learned_theta, - percent_missing=0, - percent_noisy=0, + percent_missing=0.50, + percent_noisy=0.50, + replace=True ) - model = Extract(observations, modes.AMDN, occ_threshold = 1) + model = Extract(observations, modes.AMDN, debug=True, occ_threshold = 1) f = open("results.txt", "w") f.write(model.details()) From e0d114690ebcb9a5b18271cd21fda10794792e48 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Wed, 25 Aug 2021 15:27:19 -0400 Subject: [PATCH 142/181] debug disorder constraints --- macq/extract/amdn.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 51f2c78d..9a20cba3 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -78,18 +78,9 @@ def _extract_aux_set_weights(cnf_formula: And[Or[Var]], constraints: Dict, prob_ # find all the auxiliary variables for clause in cnf_formula.children: for var in clause.children: - if isinstance(var.name, Aux): - # only have the case where the clause is part of an aux <-> formula if the other variables are not aux - valid = True - for other in clause.children - {var}: - if isinstance(other.name, Aux): - valid = False - break - if valid: - # aux variables are the soft clauses that get the original weight - constraints[AMDN._or_refactor(var)] = prob_disordered * WMAX - else: - break + if isinstance(var.name, Aux) and var.true: + # aux variables are the soft clauses that get the original weight + constraints[AMDN._or_refactor(var)] = prob_disordered * WMAX # set each original constraint to be a hard clause constraints[clause] = "HARD" @@ -169,7 +160,6 @@ def _build_disorder_constraints(obs_lists: ObservationLists, debug: int): print(disorder_constraints[c]) break print() - return disorder_constraints @staticmethod @@ -367,7 +357,7 @@ def _extract_raw_model(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hasha @staticmethod def _split_raw_fluent(raw_f: Hashable, learned_actions: Dict[str, LearnedAction]): - raw_f = str(raw_f) + raw_f = str(raw_f)[1:-1] pre_str = " is a precondition of " add_str = " is added by " del_str = " is deleted by " From b3c5defbc299cfac72865bd1f15a2494d7dcd30a Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 16:39:55 -0400 Subject: [PATCH 143/181] Add Observer usage documentation --- .gitignore | 3 ++- README.md | 26 -------------------------- docs/extract/observer.md | 37 +++++++++++++++++++++++++++++++++++++ macq/extract/observer.py | 9 ++++++++- 4 files changed, 47 insertions(+), 28 deletions(-) create mode 100644 docs/extract/observer.md diff --git a/.gitignore b/.gitignore index eae90acb..74d2c9b4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ __init__.pyc generated_testing_files/ new_domain.pddl new_prob.pddl -test_model.json \ No newline at end of file +test_model.json +html/ diff --git a/README.md b/README.md index 0bee1b8a..146cb8bd 100644 --- a/README.md +++ b/README.md @@ -40,32 +40,6 @@ trace.get_pre_states(action) # get the state before each occurance of action trace.get_post_states(action) # state after each occurance of action trace.get_total_cost() -###################################################################### -# Model Extraction - OBSERVER Technique -###################################################################### -observations = traces.tokenize(IdentityObservation) -model = extract.Extract(observations, extract.modes.OBSERVER) -model.details() - -Model: - Fluents: at stone stone-03 location pos-04-06, at stone stone-01 location pos-04-06, at stone stone-02 location pos-05-06, at stone stone-06 location pos-07-04, at stone stone-11 ... - Actions: - push-to-goal stone stone-04 location pos-04-05 location pos-04-06 direction dir-up location pos-04-04 player player-01: - precond: - at player player-01 location pos-04-06 - at stone stone-04 location pos-04-05 - clear location pos-05-06 - ... - add: - at stone stone-04 location pos-04-04 - clear location pos-04-06 - at-goal stone stone-04 - at player player-01 location pos-04-05 - delete: - at stone stone-04 location pos-04-05 - clear location pos-04-04 - at player player-01 location pos-04-06 - ... ###################################################################### # Model Extraction - SLAF Technique ###################################################################### diff --git a/docs/extract/observer.md b/docs/extract/observer.md new file mode 100644 index 00000000..c42dc2c1 --- /dev/null +++ b/docs/extract/observer.md @@ -0,0 +1,37 @@ +# Example Usage + +```python +from macq import generate, extract +from macq.observation import IdentityObservation + +traces = generate.pddl.VanillaSampling( problem_id=123, plan_len=20, num_traces=100).traces +observations = traces.tokenize(IdentityObservation) +model = extract.Extract(observations, extract.modes.OBSERVER) + +print(model.details()) +``` + +**Output:** +``` +Model: + Fluents: at stone stone-03 location pos-04-06, at stone stone-01 location pos-04-06, at stone stone-02 location pos-05-06, at stone stone-06 location pos-07-04, at stone stone-11 ... + Actions: + push-to-goal stone stone-04 location pos-04-05 location pos-04-06 direction dir-up location pos-04-04 player player-01: + precond: + at player player-01 location pos-04-06 + at stone stone-04 location pos-04-05 + clear location pos-05-06 + ... + add: + at stone stone-04 location pos-04-04 + clear location pos-04-06 + at-goal stone stone-04 + at player player-01 location pos-04-05 + delete: + at stone stone-04 location pos-04-05 + clear location pos-04-04 + at player player-01 location pos-04-06 + ... +``` + +# API Documentation diff --git a/macq/extract/observer.py b/macq/extract/observer.py index e93d0bdd..7e160d15 100644 --- a/macq/extract/observer.py +++ b/macq/extract/observer.py @@ -1,3 +1,7 @@ +""" +.. include:: ../../docs/extract/observer.md +""" + from typing import List, Set from collections import defaultdict @@ -54,7 +58,10 @@ def _get_fluents(obs_lists: ObservationLists): for obs_list in obs_lists: for obs in obs_list: # Update fluents with the fluents in this observation - fluents.update(LearnedFluent(f.name, [o.details() for o in f.objects]) for f in obs.state.keys()) + fluents.update( + LearnedFluent(f.name, [o.details() for o in f.objects]) + for f in obs.state.keys() + ) return fluents @staticmethod From 51cb92ef757795c00d01739543ec00ef0e18bdf5 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 17:09:11 -0400 Subject: [PATCH 144/181] Add index page --- docs/index.md | 17 +++++++++++++++++ macq/__init__.py | 3 +++ 2 files changed, 20 insertions(+) create mode 100644 docs/index.md diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..ae954d30 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,17 @@ +# Usage Documentation + +## Trace Generation +- [VanillaSampling](extract/observer/#usage) + +## Tokenization +- [IdentityObservation](extract/observer#usage) +- [AtomicPartialObservation](extract/slaf#usage) + +## Extraction Techniques +- [Observer](extract/observer#usage) +- [SLAF](extract/slaf#usage) +- [ARMS](extract/arms#usage) +- [AMDN](extract/amdn#usage) + + +# API Documentation diff --git a/macq/__init__.py b/macq/__init__.py index e69de29b..0fe09475 100644 --- a/macq/__init__.py +++ b/macq/__init__.py @@ -0,0 +1,3 @@ +""" +.. include:: ../docs/index.md +""" From 13c38ac58f8e8c5774ed7e140ea7911b145393b2 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 17:09:41 -0400 Subject: [PATCH 145/181] Add SLAF usage --- README.md | 18 ------------------ docs/extract/observer.md | 6 +++--- docs/extract/slaf.md | 26 ++++++++++++++++++++++++++ macq/extract/slaf.py | 9 +++++++-- 4 files changed, 36 insertions(+), 23 deletions(-) create mode 100644 docs/extract/slaf.md diff --git a/README.md b/README.md index 146cb8bd..cc26d901 100644 --- a/README.md +++ b/README.md @@ -39,24 +39,6 @@ trace.actions trace.get_pre_states(action) # get the state before each occurance of action trace.get_post_states(action) # state after each occurance of action trace.get_total_cost() - -###################################################################### -# Model Extraction - SLAF Technique -###################################################################### -traces = generate.pddl.VanillaSampling(problem_id = 123, plan_len = 2, num_traces = 1).traces -observations = traces.tokenize(PartialObservabilityToken, method=PartialObservabilityToken.random_subset, percent_missing=0.10) -model = Extract(observations, modes.SLAF) -model.details() - -Model: - Fluents: clear location pos-06-09, clear location pos-02-05, clear location pos-08-08, clear location pos-10-05, clear location pos-02-06, clear location pos-10-02, clear location pos-01-01, at stone stone-05 location pos-08-05, at stone stone-07 location pos-08-06, at stone stone-03 location pos-07-04, clear location pos-03-06, clear location pos-10-06, clear location pos-10-10, clear location pos-05-09, clear location pos-05-07, clear location pos-02-07, clear location pos-09-01, at stone stone-06 location pos-04-06, clear location pos-02-03, clear location pos-07-05, clear location pos-09-10, clear location pos-06-05, at stone stone-01 location pos-05-04, clear location pos-02-10, clear location pos-06-10, clear location pos-11-03, at stone stone-11 location pos-06-08, at stone stone-08 location pos-04-07, clear location pos-01-10, clear location pos-07-03, clear location pos-02-11, clear location pos-03-01, clear location pos-06-02, clear location pos-03-02, clear location pos-11-01, clear location pos-06-03, clear location pos-08-04, clear location pos-09-11, at stone stone-09 location pos-08-07, clear location pos-09-07, clear location pos-06-07, clear location pos-10-01, clear location pos-11-09, clear location pos-03-05, clear location pos-07-06, clear location pos-05-05, at stone stone-12 location pos-07-08, clear location pos-10-03, clear location pos-11-11, clear location pos-10-09, clear location pos-02-01, clear location pos-02-02, clear location pos-01-02, at stone stone-02 location pos-06-04, clear location pos-03-10, clear location pos-05-10, clear location pos-07-10, clear location pos-09-05, clear location pos-07-09, clear location pos-05-03, clear location pos-10-11, clear location pos-01-03, at stone stone-04 location pos-04-05, clear location pos-07-02, clear location pos-09-06, clear location pos-10-07, clear location pos-01-09, clear location pos-03-07, clear location pos-04-04, clear location pos-01-11 - Actions: - move player player-01 direction dir-left location pos-05-02 location pos-06-02: - precond: - add: - delete: - (clear location pos-05-02) - (at player player-01 location pos-06-02) ``` ## Coverage diff --git a/docs/extract/observer.md b/docs/extract/observer.md index c42dc2c1..2bcef454 100644 --- a/docs/extract/observer.md +++ b/docs/extract/observer.md @@ -1,10 +1,10 @@ -# Example Usage +# Usage ```python from macq import generate, extract from macq.observation import IdentityObservation -traces = generate.pddl.VanillaSampling( problem_id=123, plan_len=20, num_traces=100).traces +traces = generate.pddl.VanillaSampling(problem_id=123, plan_len=20, num_traces=100).traces observations = traces.tokenize(IdentityObservation) model = extract.Extract(observations, extract.modes.OBSERVER) @@ -12,7 +12,7 @@ print(model.details()) ``` **Output:** -``` +```text Model: Fluents: at stone stone-03 location pos-04-06, at stone stone-01 location pos-04-06, at stone stone-02 location pos-05-06, at stone stone-06 location pos-07-04, at stone stone-11 ... Actions: diff --git a/docs/extract/slaf.md b/docs/extract/slaf.md new file mode 100644 index 00000000..f333eefe --- /dev/null +++ b/docs/extract/slaf.md @@ -0,0 +1,26 @@ +# Usage + +```python +from macq import generate, extract +from macq.observation import AtomicPartialObservation + +traces = generate.pddl.VanillaSampling(problem_id=123, plan_len=2, num_traces=1).traces +observations = traces.tokenize(AtomicPartialObservation, percent_missing=0.10) +model = Extract(observations, extract.modes.SLAF) +print(model.details()) +``` + +**Output:** +```text +Model: + Fluents: clear location pos-06-09, clear location pos-02-05, clear location pos-08-08, clear location pos-10-05, clear location pos-02-06, clear location pos-10-02, clear location pos-01-01, at stone stone-05 location pos-08-05, at stone stone-07 location pos-08-06, at stone stone-03 location pos-07-04, clear location pos-03-06, clear location pos-10-06, clear location pos-10-10, clear location pos-05-09, clear location pos-05-07, clear location pos-02-07, clear location pos-09-01, at stone stone-06 location pos-04-06, clear location pos-02-03, clear location pos-07-05, clear location pos-09-10, clear location pos-06-05, at stone stone-01 location pos-05-04, clear location pos-02-10, clear location pos-06-10, clear location pos-11-03, at stone stone-11 location pos-06-08, at stone stone-08 location pos-04-07, clear location pos-01-10, clear location pos-07-03, clear location pos-02-11, clear location pos-03-01, clear location pos-06-02, clear location pos-03-02, clear location pos-11-01, clear location pos-06-03, clear location pos-08-04, clear location pos-09-11, at stone stone-09 location pos-08-07, clear location pos-09-07, clear location pos-06-07, clear location pos-10-01, clear location pos-11-09, clear location pos-03-05, clear location pos-07-06, clear location pos-05-05, at stone stone-12 location pos-07-08, clear location pos-10-03, clear location pos-11-11, clear location pos-10-09, clear location pos-02-01, clear location pos-02-02, clear location pos-01-02, at stone stone-02 location pos-06-04, clear location pos-03-10, clear location pos-05-10, clear location pos-07-10, clear location pos-09-05, clear location pos-07-09, clear location pos-05-03, clear location pos-10-11, clear location pos-01-03, at stone stone-04 location pos-04-05, clear location pos-07-02, clear location pos-09-06, clear location pos-10-07, clear location pos-01-09, clear location pos-03-07, clear location pos-04-04, clear location pos-01-11 + Actions: + move player player-01 direction dir-left location pos-05-02 location pos-06-02: + precond: + add: + delete: + (clear location pos-05-02) + (at player player-01 location pos-06-02) +``` + +# API Documentation diff --git a/macq/extract/slaf.py b/macq/extract/slaf.py index ada95794..e759178e 100644 --- a/macq/extract/slaf.py +++ b/macq/extract/slaf.py @@ -1,3 +1,6 @@ +""" +.. include:: ../../docs/extract/slaf.md +""" import macq.extract as extract from typing import Set, Union from nnf import Var, Or, And, true, false, config @@ -167,7 +170,9 @@ def __sort_results(observations: ObservationLists, entailed: Set): # iterate through each step for o in observations: for token in o: - model_fluents.update([LearnedFluent(name=f, objects=[]) for f in token.state]) + model_fluents.update( + [LearnedFluent(name=f, objects=[]) for f in token.state] + ) # if an action was taken on this step if token.action: # set up a base LearnedAction with the known information @@ -356,7 +361,7 @@ def __as_strips_slaf(o_list: ObservationLists): phi["pos expl"] = set() phi["neg expl"] = set() - """Steps 1 (a-c) - Update every fluent in the fluent-factored transition belief formula + """Steps 1 (a-c) - Update every fluent in the fluent-factored transition belief formula with information from the last step.""" """Step 1 (a) - update the neutral effects.""" From 20a8cbfd0af03c51468b984b8783a3064ec2b590 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 17:10:09 -0400 Subject: [PATCH 146/181] Add AMDN usage section --- docs/extract/amdn.md | 13 +++++++++++++ macq/extract/amdn.py | 16 +++++++++++----- 2 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 docs/extract/amdn.md diff --git a/docs/extract/amdn.md b/docs/extract/amdn.md new file mode 100644 index 00000000..a31a4f8f --- /dev/null +++ b/docs/extract/amdn.md @@ -0,0 +1,13 @@ +# Usage + +```python +from macq import generate, extract + +print(model.details()) +``` + +**Output:** +```text +``` + +# API Documentation diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 3981f357..520cd118 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -1,8 +1,13 @@ +""" +.. include:: ../../docs/extract/amdn.md +""" + import macq.extract as extract from .model import Model from ..trace import ObservationLists from ..observation import NoisyPartialDisorderedParallelObservation + class AMDN: def __new__(cls, obs_lists: ObservationLists): """Creates a new Model object. @@ -23,7 +28,7 @@ def __new__(cls, obs_lists: ObservationLists): # (this will make it easy and efficient to refer to later, and prevents unnecessary recalculations). store as attribute # also create a list of all tuples, store as attribute - #return Model(fluents, actions) + # return Model(fluents, actions) def _build_disorder_constraints(self): # TODO: @@ -66,12 +71,12 @@ def _build_noise_constraints(self): # iterate through all tuples # for each tuple: iterate through each step over ALL the plan traces # count the number of occurrences; if higher than the user-provided parameter delta, - # store this tuple as a dictionary entry in a list of dictionaries (something like + # store this tuple as a dictionary entry in a list of dictionaries (something like # [{"action and proposition": , "occurrences of r:" 5}]). # after all iterations are through, iterate through all the tuples in this dictionary, # and set [constraint 6] with the calculated weight. # TODO: Ask - what "occurrences of all propositions" refers to - is it the total number of steps...? - + # store the initial state s0 # iterate through every step in the plan trace # at each step, check all the propositions r in the current state @@ -80,7 +85,7 @@ def _build_noise_constraints(self): # continuing the process with different propositions? Do we still count the occurrences of each proposition through # the entire trace to use when we calculate the weight? - # [constraint 8] is almost identical to [constraint 6]. Watch the order of the tuples. + # [constraint 8] is almost identical to [constraint 6]. Watch the order of the tuples. pass @@ -92,4 +97,5 @@ def _solve_constraints(self): def _convert_to_model(self): # TODO: # convert the result to a Model - pass \ No newline at end of file + pass + From 86a7f0502234fbb59e6e4c15ac050c752c05b0ed Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 17:10:27 -0400 Subject: [PATCH 147/181] Add ARMS usage --- docs/extract/arms.md | 56 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/extract/arms.md diff --git a/docs/extract/arms.md b/docs/extract/arms.md new file mode 100644 index 00000000..8de088fe --- /dev/null +++ b/docs/extract/arms.md @@ -0,0 +1,56 @@ +# Usage + +```python +from macq import generate, extract +from macq.trace import PlanningObject, Fluent, TraceList +from macq.observation import PartialObservation + +def get_fluent(name: str, objs: list[str]): + objects = [PlanningObject(o.split()[0], o.split()[1]) for o in objs] + return Fluent(name, objects) + +traces = TraceList() +generator = generate.pddl.TraceFromGoal(problem_id=1801) + +generator.change_goal( + { + get_fluent("communicated_soil_data", ["waypoint waypoint2"]), + get_fluent("communicated_rock_data", ["waypoint waypoint3"]), + get_fluent( + "communicated_image_data", ["objective objective1", "mode high_res"] + ), + } +) +traces.append(generator.generate_trace()) + +generator.change_goal( + { + get_fluent("communicated_soil_data", ["waypoint waypoint2"]), + get_fluent("communicated_rock_data", ["waypoint waypoint3"]), + get_fluent( + "communicated_image_data", ["objective objective1", "mode high_res"] + ), + } +) +traces.append(generator.generate_trace()) + +observations = traces.tokenize(PartialObservation, percent_missing=0.60) +model = extract.Extract( + observations, + extract.modes.ARMS, + upper_bound=2, + min_support=2, + action_weight=110, + info_weight=100, + threshold=0.6, + info3_default=30, + plan_default=30, +) +model.details() +``` + +**Output:** +```text +``` + +# API Documentation From 03fa7b0db8a8ddc6f7b989078c5c7f413fe09e63 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 17:11:33 -0400 Subject: [PATCH 148/181] Ignore docs folder --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index eae90acb..74d2c9b4 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ __init__.pyc generated_testing_files/ new_domain.pddl new_prob.pddl -test_model.json \ No newline at end of file +test_model.json +html/ From f0d836fe92c554c567e968dba4fbcbe64ed9dad2 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 17:20:49 -0400 Subject: [PATCH 149/181] Document ObservationLists init docstring --- macq/trace/observation_lists.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/macq/trace/observation_lists.py b/macq/trace/observation_lists.py index 72ffd5ad..fb80ca6c 100644 --- a/macq/trace/observation_lists.py +++ b/macq/trace/observation_lists.py @@ -31,6 +31,16 @@ def __init__( Token: Type[Observation] = None, **kwargs, ): + """Initializes an ObservationLists object from either a `TraceList` or a 2D list of `Observation`. + + If `traces` is a `TraceList`, a `Token` class is required to tokenize the `TraceList`. + + Args: + traces (TraceList | List[List[Observation]]): + Either a `TraceList` or a 2D list of observations. + Token (Type[Observation]): + A child class of `Observation`. Used to tokenize traces if it is a `TraceList`. + """ if isinstance(traces, TraceAPI.TraceList): if not Token: raise MissingToken() From 9e8137f93da38b1214568fcb908d794cf0e74c2c Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 17:35:30 -0400 Subject: [PATCH 150/181] Add documentation for pysat utils module --- macq/utils/pysat.py | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py index 86ebaa4c..368da671 100644 --- a/macq/utils/pysat.py +++ b/macq/utils/pysat.py @@ -6,12 +6,36 @@ def get_encoding( clauses: And[Or[Var]], start: int = 1 ) -> Tuple[Dict[Hashable, int], Dict[int, Hashable]]: + """Maps NNF clauses to pysat clauses and vice-versa. + + Args: + clauses (And[Or[Var]]): + NNF clauses (in conjunctive normal form) to be mapped to pysat clauses. + start (int): + Optional; The number to start the mapping from. Defaults to 1. + + Returns: + Tuple[Dict[Hashable, int], Dict[int, Hashable]]: + The encode mapping (NNF to pysat), and the decode mapping (pysat to NNF). + """ decode = dict(enumerate(clauses.vars(), start=start)) encode = {v: k for k, v in decode.items()} return encode, decode def encode(clauses: And[Or[Var]], encode: Dict[Hashable, int]) -> List[List[int]]: + """Encodes NNF clauses into pysat clauses. + + Args: + clauses (And[Or[Var]]): + NNF clauses (in conjunctive normal form) to be converted to pysat clauses. + encode (Dict[Hashable, int]): + The encode mapping to apply to the NNF clauses. + + Returns: + List[List[int]]: + The pysat encoded clauses. + """ encoded = [ [encode[var.name] if var.true else -encode[var.name] for var in clause] for clause in clauses @@ -22,7 +46,20 @@ def encode(clauses: And[Or[Var]], encode: Dict[Hashable, int]) -> List[List[int] def to_wcnf( soft_clauses: And[Or[Var]], weights: List[int], hard_clauses: And[Or[Var]] = None ) -> Tuple[WCNF, Dict[int, Hashable]]: - """Converts a python-nnf CNF formula to a pysat WCNF.""" + """Builds a pysat weighted CNF theory from pysat clauses. + + Args: + soft_clauses (And[Or[Var]]): + The soft clauses (NNF clauses, in CNF) for the WCNF theory. + weights (List[int]): + The weights to associate with the soft clauses. + hard_clauses (And[Or[Var]]): + Optional; Hard clauses (unweighted) to add to the WCNF theory. + + Returns: + Tuple[WCNF, Dict[int, Hashable]]: + The WCNF theory, and the decode mapping to convert the pysat vars back to NNF. + """ wcnf = WCNF() soft_encode, decode = get_encoding(soft_clauses) encoded = encode(soft_clauses, soft_encode) From b27316b649576f077e6771883edeca8357f85cfd Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 17:48:15 -0400 Subject: [PATCH 151/181] Cleanup api response handling --- macq/generate/pddl/generator.py | 45 +++++++++--------------- macq/observation/identity_observation.py | 3 -- macq/observation/partial_observation.py | 3 -- macq/trace/trace.py | 4 +-- 4 files changed, 18 insertions(+), 37 deletions(-) diff --git a/macq/generate/pddl/generator.py b/macq/generate/pddl/generator.py index 3249851a..aea73c12 100644 --- a/macq/generate/pddl/generator.py +++ b/macq/generate/pddl/generator.py @@ -25,8 +25,7 @@ class PlanningDomainsAPIError(Exception): """Raised when a valid response cannot be obtained from the planning.domains solver.""" - def __init__(self, message, e): - self.e = e + def __init__(self, message): super().__init__(message) @@ -395,34 +394,24 @@ def generate_plan(self, from_ipc_file: bool = False, filename: str = None): "problem": open(self.pddl_prob, "r").read(), } - def get_api_response(): - resp = requests.post( - "http://solver.planning.domains/solve", verify=False, json=data - ).json() - return [act["name"] for act in resp["result"]["plan"]] - - try: - plan = get_api_response() - except TypeError: - try: - plan = get_api_response() - except TypeError: + def get_api_response(delays: List[int]): + if delays: + sleep(delays[0]) try: - sleep(1) - plan = get_api_response() + resp = requests.post( + "http://solver.planning.domains/solve", + verify=False, + json=data, + ).json() + return [act["name"] for act in resp["result"]["plan"]] except TypeError: - try: - sleep(5) - plan = get_api_response() - except TypeError: - try: - sleep(10) - plan = get_api_response() - except TypeError as e: - raise PlanningDomainsAPIError( - "Could not get a valid response from the planning.domains solver after 5 attempts.", - e, - ) + return get_api_response(delays[1:]) + + plan = get_api_response([0, 1, 3, 5, 10]) + if plan is None: + raise PlanningDomainsAPIError( + "Could not get a valid response from the planning.domains solver after 5 attempts.", + ) else: f = open(filename, "r") diff --git a/macq/observation/identity_observation.py b/macq/observation/identity_observation.py index 65bc5ee5..6989340b 100644 --- a/macq/observation/identity_observation.py +++ b/macq/observation/identity_observation.py @@ -43,9 +43,6 @@ def __init__(self, step: Step, **kwargs): self.state = step.state.clone() self.action = None if step.action is None else step.action.clone() - def __hash__(self): - return super().__hash__() - def __eq__(self, other): return ( isinstance(other, IdentityObservation) diff --git a/macq/observation/partial_observation.py b/macq/observation/partial_observation.py index 895769ae..41e7fa01 100644 --- a/macq/observation/partial_observation.py +++ b/macq/observation/partial_observation.py @@ -47,9 +47,6 @@ def __init__( self.state = None if percent_missing == 1 else step.state.clone() self.action = None if step.action is None else step.action.clone() - def __hash__(self): - return super().__hash__() - def __eq__(self, other): return ( isinstance(other, PartialObservation) diff --git a/macq/trace/trace.py b/macq/trace/trace.py index 440ad437..314bf1b3 100644 --- a/macq/trace/trace.py +++ b/macq/trace/trace.py @@ -55,9 +55,7 @@ def __init__(self, steps: List[Step] = None): self.__reinit_actions_and_fluents() def __eq__(self, other): - if not isinstance(other, Trace): - return False - return self.steps == other.steps + return isinstance(other, Trace) and self.steps == other.steps def __len__(self): return len(self.steps) From 01d55be4485635e834dcf7a0a106a10b136e1fa8 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 17:55:16 -0400 Subject: [PATCH 152/181] Re-add hash function to IdentityObservation --- macq/observation/identity_observation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/macq/observation/identity_observation.py b/macq/observation/identity_observation.py index 6989340b..65bc5ee5 100644 --- a/macq/observation/identity_observation.py +++ b/macq/observation/identity_observation.py @@ -43,6 +43,9 @@ def __init__(self, step: Step, **kwargs): self.state = step.state.clone() self.action = None if step.action is None else step.action.clone() + def __hash__(self): + return super().__hash__() + def __eq__(self, other): return ( isinstance(other, IdentityObservation) From 9116287f7d5b98792d4c372519d8fb093881875b Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 17:55:47 -0400 Subject: [PATCH 153/181] Import pysat functionality from pysat utils module --- macq/extract/arms.py | 4 +--- macq/utils/pysat.py | 1 + 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index ce1b4317..74754c03 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -3,8 +3,6 @@ from warnings import warn from typing import Set, List, Dict, Tuple, Hashable from nnf import Var, And, Or, false as nnffalse -from pysat.examples.rc2 import RC2 -from pysat.formula import WCNF from . import LearnedAction, Model from .exceptions import ( IncompatibleObservationToken, @@ -12,7 +10,7 @@ ) from ..observation import PartialObservation as Observation from ..trace import ObservationLists, Fluent, Action # Action only used for typing -from ..utils.pysat import to_wcnf +from ..utils.pysat import to_wcnf, RC2, WCNF @dataclass diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py index 368da671..5a9cf2a5 100644 --- a/macq/utils/pysat.py +++ b/macq/utils/pysat.py @@ -1,5 +1,6 @@ from typing import List, Tuple, Dict, Hashable from pysat.formula import WCNF +from pysat.examples.rc2 import RC2 from nnf import And, Or, Var From 6e150b88a86e5ed321c4b4c3f862db247a2fc481 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 18:00:24 -0400 Subject: [PATCH 154/181] Add steps to ARMS docstrings --- macq/extract/arms.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 74754c03..a89460e1 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -255,7 +255,7 @@ def _step1( Dict[LearnedAction, Dict[LearnedAction, Set[str]]], Dict[Action, LearnedAction], ]: - """Substitute instantiated objects in each action instance with the object type.""" + """(Step 1) Substitute instantiated objects in each action instance with the object type.""" learned_actions: Set[LearnedAction] = set() action_map: Dict[Action, LearnedAction] = {} @@ -293,7 +293,7 @@ def _step2( min_support: int, debug: bool, ) -> Tuple[ARMSConstraints, Dict[Fluent, Relation]]: - """Generate action constraints, information constraints, and plan constraints.""" + """(Step 2) Generate action constraints, information constraints, and plan constraints.""" # Map fluents to relations # relations are fluents but with instantiated objects replaced by the object type @@ -347,6 +347,7 @@ def _step2A( relations: Set[Relation], debug: bool, ) -> List[Or[Var]]: + """(Step 2 - Action Constraints)""" if debug: print("\nBuilding action constraints...\n") @@ -418,6 +419,7 @@ def _step2I( actions: Dict[Action, LearnedAction], debug: bool, ) -> Tuple[List[Or[Var]], Dict[Or[Var], int]]: + """(Step 2 - Info Constraints)""" if debug: print("\nBuilding information constraints...") @@ -557,6 +559,7 @@ def _step2P( min_support: int, debug: bool, ) -> Dict[Or[Var], int]: + """(Step 2 - Plan Constraints)""" frequent_pairs = ARMS._apriori( [ [ @@ -615,7 +618,7 @@ def _step3( plan_default: int, debug: bool, ) -> Tuple[WCNF, Dict[int, Hashable]]: - """Construct the weighted MAX-SAT problem.""" + """(Step 3) Construct the weighted MAX-SAT problem.""" action_weights = [action_weight] * len(constraints.action) info_weights = [info_weight] * len(constraints.info) @@ -675,6 +678,7 @@ def get_support_rate(count): @staticmethod def _step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: + """(Step 4) Build the MAX-SAT theory.""" solver = RC2(max_sat) encoded_model = solver.compute() @@ -696,6 +700,7 @@ def _step5( actions: List[LearnedAction], debug: bool, ): + """(Step 5) Extract the learned action effects from the solved model.""" action_map = {a.details(): a for a in actions} negative_constraints = defaultdict(set) plan_constraints: List[Tuple[str, LearnedAction, LearnedAction]] = [] From 96d41a3f269bee40908609a75e3a2082f6c60000 Mon Sep 17 00:00:00 2001 From: ecal Date: Wed, 25 Aug 2021 18:04:43 -0400 Subject: [PATCH 155/181] Replace ternary operators with dictionary redirect --- macq/extract/arms.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index a89460e1..099758f6 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -721,22 +721,18 @@ def _step5( f"Learned constraint: {relation} in {effect}_{action.details()}" ) if val: - action_update = ( - action.update_precond - if effect == "pre" - else action.update_add - if effect == "add" - else action.update_delete - ) + action_update = { + "pre": action.update_precond, + "add": action.update_add, + "del": action.update_delete, + }[effect] action_update({relation}) else: - action_effect = ( - action.precond - if effect == "pre" - else action.add - if effect == "add" - else action.delete - ) + action_effect = { + "pre": action.precond, + "add": action.add, + "del": action.delete, + }[effect] if relation in action_effect: if debug: warn( From 7b0732d1bac6eff481c1726bcf7e9c1fd54896dd Mon Sep 17 00:00:00 2001 From: beckydvn Date: Thu, 26 Aug 2021 11:27:48 -0400 Subject: [PATCH 156/181] add debug mode to observe certain fluents --- macq/extract/amdn.py | 180 +++++++++++++++++++++++++------------ tests/extract/test_amdn.py | 6 +- 2 files changed, 126 insertions(+), 60 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 9a20cba3..65b9bb6d 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -1,7 +1,7 @@ from macq.extract.learned_action import LearnedAction from nnf.operators import implies import macq.extract as extract -from typing import Dict, Union, Set, Hashable +from typing import Dict, List, Union, Set, Hashable from nnf import Aux, Var, And, Or from pysat.formula import WCNF from pysat.examples.rc2 import RC2 @@ -56,7 +56,7 @@ def __new__(cls, obs_lists: ObservationLists, debug: bool = False, occ_threshold @staticmethod def _amdn(obs_lists: ObservationLists, debug: int, occ_threshold: int): - wcnf, decode = AMDN._solve_constraints(obs_lists, debug, occ_threshold) + wcnf, decode = AMDN._solve_constraints(obs_lists, occ_threshold, debug) raw_model = AMDN._extract_raw_model(wcnf, decode) return AMDN._extract_model(obs_lists, raw_model) @@ -85,10 +85,85 @@ def _extract_aux_set_weights(cnf_formula: And[Or[Var]], constraints: Dict, prob_ constraints[clause] = "HARD" @staticmethod - def _build_disorder_constraints(obs_lists: ObservationLists, debug: int): - global e - + def _get_observe(obs_lists: ObservationLists): + print("Select a proposition to observe:") + sorted_f = [str(f) for f in obs_lists.propositions] + sorted_f.sort() + for f in sorted_f: + print(f) + to_obs = [] + user_input = "" + while user_input != "x": + user_input = input( + "Which fluents do you want to observe? Enter 'x' when you are finished.\n" + ) + if user_input in sorted_f: + to_obs.append(user_input[1:-1]) + print(user_input + " added to the debugging list.") + else: + if user_input != "x": + print("The fluent you entered is invalid.") + return to_obs + + @staticmethod + def _debug_is_observed(constraint: Or, to_obs: List[str]): + observe = False + for c in constraint.children: + for v in to_obs: + if v in str(c): + observe = True + return observe + + @staticmethod + def _debug_simple_pprint(constraints: Dict, to_obs: List[str]): + for c in constraints: + observe = AMDN._debug_is_observed(c, to_obs) + if observe: + e.pprint(e, c) + print(constraints[c]) + + @staticmethod + def _debug_aux_pprint(constraints: Dict, to_obs: List[str]): + aux_map = {} + index = 0 + for c in constraints: + observe = AMDN._debug_is_observed(c, to_obs) + if observe: + for var in c.children: + if isinstance(var.name, Aux) and var.name not in aux_map: + aux_map[var.name] = f"aux {index}" + index += 1 + + all_pretty_c = {} + for c in constraints: + observe = AMDN._debug_is_observed(c, to_obs) + if observe: + pretty_c = [] + for var in c.children: + if isinstance(var.name, Aux): + if var.true: + pretty_c.append(Var(aux_map[var.name])) + else: + pretty_c.append(~Var(aux_map[var.name])) + else: + pretty_c.append(var) + # map disorder constraints to pretty disorder constraints + all_pretty_c[c] = Or(pretty_c) + + for aux in aux_map.values(): + for c, v in all_pretty_c.items(): + for child in v.children: + if aux == child.name: + e.pprint(e, v) + print(constraints[c]) + break + print() + + + @staticmethod + def _build_disorder_constraints(obs_lists: ObservationLists): disorder_constraints = {} + # iterate through all traces for i in range(len(obs_lists.all_par_act_sets)): # get the parallel action sets for this trace @@ -126,44 +201,10 @@ def _build_disorder_constraints(obs_lists: ObservationLists, debug: int): disjunct_all_constr_2 = Or(constraint_2).to_CNF() AMDN._extract_aux_set_weights(disjunct_all_constr_1, disorder_constraints, (1 - p)) AMDN._extract_aux_set_weights(disjunct_all_constr_2, disorder_constraints, p) - - if debug: - aux_map = {} - print("\nFor the pair: " + act_x.details() + " and " + act_y.details() + ":") - print("DC:\n") - index = 0 - for c in disorder_constraints: - for var in c.children: - if isinstance(var.name, Aux) and var.name not in aux_map: - aux_map[var.name] = f"aux {index}" - index += 1 - - all_pretty_c = {} - for c in disorder_constraints: - pretty_c = [] - for var in c.children: - if isinstance(var.name, Aux): - if var.true: - pretty_c.append(Var(aux_map[var.name])) - else: - pretty_c.append(~Var(aux_map[var.name])) - else: - pretty_c.append(var) - # map disorder constraints to pretty disorder constraints - all_pretty_c[c] = Or(pretty_c) - - for aux in aux_map.values(): - for c, v in all_pretty_c.items(): - for child in v.children: - if aux == child.name: - e.pprint(e, v) - print(disorder_constraints[c]) - break - print() - return disorder_constraints + return disorder_constraints @staticmethod - def _build_hard_parallel_constraints(obs_lists: ObservationLists, debug: int): + def _build_hard_parallel_constraints(obs_lists: ObservationLists): hard_constraints = {} # create a list of all tuples for act in obs_lists.actions: @@ -174,7 +215,7 @@ def _build_hard_parallel_constraints(obs_lists: ObservationLists, debug: int): return hard_constraints @staticmethod - def _build_soft_parallel_constraints(obs_lists: ObservationLists, debug: int): + def _build_soft_parallel_constraints(obs_lists: ObservationLists): soft_constraints = {} # NOTE: the paper does not take into account possible conflicts between the preconditions of actions @@ -193,7 +234,7 @@ def _build_soft_parallel_constraints(obs_lists: ObservationLists, debug: int): p = obs_lists.probabilities[ActionPair({act_x, act_x_prime})] # iterate through all propositions for r in obs_lists.propositions: - soft_constraints[implies(add(r, act_x), ~delete(r, act_x_prime))] = (1 - p) * WMAX + soft_constraints[implies(add(r, act_x), ~delete(r, act_x_prime))] = (1 - p) * WMAX # iterate through all traces for i in range(len(obs_lists.all_par_act_sets)): @@ -207,11 +248,19 @@ def _build_soft_parallel_constraints(obs_lists: ObservationLists, debug: int): # iterate through all propositions and similarly set the constraint for r in obs_lists.propositions: soft_constraints[implies(add(r, act_y), ~delete(r, act_x_prime))] = p * WMAX + return soft_constraints @staticmethod - def _build_parallel_constraints(obs_lists: ObservationLists, debug: int): - return {**AMDN._build_hard_parallel_constraints(obs_lists, debug), **AMDN._build_soft_parallel_constraints(obs_lists, debug)} + def _build_parallel_constraints(obs_lists: ObservationLists, debug: int, to_obs: List[str]): + hard_constraints = AMDN._build_hard_parallel_constraints(obs_lists) + soft_constraints = AMDN._build_soft_parallel_constraints(obs_lists) + if debug: + print("\nHard parallel constraints:") + AMDN._debug_simple_pprint(hard_constraints, to_obs) + print("\nSoft parallel constraints:") + AMDN._debug_simple_pprint(soft_constraints, to_obs) + return {**hard_constraints, **soft_constraints} @staticmethod def _calculate_all_r_occ(obs_lists: ObservationLists): @@ -233,7 +282,7 @@ def _set_up_occurrences_dict(obs_lists: ObservationLists): return occurrences @staticmethod - def _noise_constraints_6(obs_lists: ObservationLists, debug: int, all_occ: int, occ_threshold: int): + def _noise_constraints_6(obs_lists: ObservationLists, all_occ: int, occ_threshold: int): noise_constraints_6 = {} occurrences = AMDN._set_up_occurrences_dict(obs_lists) @@ -254,11 +303,11 @@ def _noise_constraints_6(obs_lists: ObservationLists, debug: int, all_occ: int, # if the # of occurrences is higher than the user-provided threshold: if occ_r > occ_threshold: # set constraint 6 with the calculated weight - noise_constraints_6[AMDN._or_refactor(~delete(r, a))] = (occ_r / all_occ) * WMAX + noise_constraints_6[AMDN._or_refactor(~delete(r, a))] = (occ_r / all_occ) * WMAX return noise_constraints_6 @staticmethod - def _noise_constraints_7(obs_lists: ObservationLists, debug: int, all_occ: int): + def _noise_constraints_7(obs_lists: ObservationLists, all_occ: int): noise_constraints_7 = {} # set up dict occurrences = {} @@ -275,7 +324,7 @@ def _noise_constraints_7(obs_lists: ObservationLists, debug: int, all_occ: int): for i in range(len(obs_lists.all_par_act_sets)): # get the next trace/states par_act_sets = obs_lists.all_par_act_sets[i] - states = obs_lists.all_states[i] + states = obs_lists.all_states[i] # iterate through all parallel action sets within the trace for j in range(len(par_act_sets)): # examine the states before and after each parallel action set; set constraints accordinglly @@ -283,11 +332,10 @@ def _noise_constraints_7(obs_lists: ObservationLists, debug: int, all_occ: int): for r in true_prop: if not states[j][r]: noise_constraints_7[Or([add(r, act) for act in par_act_sets[j]])] = (occurrences[r]/all_occ) * WMAX - return noise_constraints_7 @staticmethod - def _noise_constraints_8(obs_lists, all_occ: int, debug: int, occ_threshold: int): + def _noise_constraints_8(obs_lists, all_occ: int, occ_threshold: int): noise_constraints_8 = {} occurrences = AMDN._set_up_occurrences_dict(obs_lists) @@ -314,18 +362,36 @@ def _noise_constraints_8(obs_lists, all_occ: int, debug: int, occ_threshold: int return noise_constraints_8 @staticmethod - def _build_noise_constraints(obs_lists: ObservationLists, debug: int, occ_threshold: int): + def _build_noise_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int, to_obs: List[str]): # calculate all occurrences for use in weights all_occ = AMDN._calculate_all_r_occ(obs_lists) - return{**AMDN._noise_constraints_6(obs_lists, debug, all_occ, occ_threshold), **AMDN._noise_constraints_7(obs_lists, debug, all_occ), **AMDN._noise_constraints_8(obs_lists, debug, all_occ, occ_threshold)} + nc_6 = AMDN._noise_constraints_6(obs_lists, all_occ, occ_threshold) + nc_7 = AMDN._noise_constraints_7(obs_lists, all_occ) + nc_8 = AMDN._noise_constraints_8(obs_lists, all_occ, occ_threshold) + if debug: + print("\nNoise constraints 6:") + AMDN._debug_simple_pprint(nc_6, to_obs) + print("\nNoise constraints 7:") + AMDN._debug_simple_pprint(nc_7, to_obs) + print("\nNoise constraints 8:") + AMDN._debug_simple_pprint(nc_8, to_obs) + return{**nc_6, **nc_7, **nc_8} @staticmethod - def _set_all_constraints(obs_lists: ObservationLists, debug: int, occ_threshold: int): - return {**AMDN._build_disorder_constraints(obs_lists, debug), **AMDN._build_parallel_constraints(obs_lists, debug), **AMDN._build_noise_constraints(obs_lists, debug, occ_threshold)} + def _set_all_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int): + if debug: + to_obs = AMDN._get_observe(obs_lists) + disorder_constraints = AMDN._build_disorder_constraints(obs_lists) + if debug: + print("\nDisorder constraints:") + AMDN._debug_aux_pprint(disorder_constraints, to_obs) + parallel_constraints = AMDN._build_parallel_constraints(obs_lists, debug, to_obs) + noise_constraints = AMDN._build_noise_constraints(obs_lists, occ_threshold, debug, to_obs) + return {**disorder_constraints, **parallel_constraints, **noise_constraints} @staticmethod - def _solve_constraints(obs_lists: ObservationLists, debug: int, occ_threshold: int): - constraints = AMDN._set_all_constraints(obs_lists, debug, occ_threshold) + def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int): + constraints = AMDN._set_all_constraints(obs_lists, occ_threshold, debug) # extract hard constraints hard_constraints = [] for c, weight in constraints.items(): diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index b6b67a5b..911d0231 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -192,10 +192,10 @@ def test_tracelist(): ObsLists=DisorderedParallelActionsObservationLists, features=features, learned_theta=learned_theta, - percent_missing=0.50, - percent_noisy=0.50, + percent_missing=0, + percent_noisy=0, replace=True ) - model = Extract(observations, modes.AMDN, debug=True, occ_threshold = 1) + model = Extract(observations, modes.AMDN, debug=True, occ_threshold = 0) f = open("results.txt", "w") f.write(model.details()) From a5e244d0c55616251912294e979dec26332d2c8c Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 26 Aug 2021 11:55:04 -0400 Subject: [PATCH 157/181] Fix ObservationLists import in ARMS --- macq/extract/arms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 099758f6..5d090be9 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -8,8 +8,8 @@ IncompatibleObservationToken, InvalidMaxSATModel, ) -from ..observation import PartialObservation as Observation -from ..trace import ObservationLists, Fluent, Action # Action only used for typing +from ..observation import PartialObservation as Observation, ObservationLists +from ..trace import Fluent, Action # Action only used for typing from ..utils.pysat import to_wcnf, RC2, WCNF From 048de7851b3e8c3fe52394ce4f75a978dd40047a Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 26 Aug 2021 13:51:30 -0400 Subject: [PATCH 158/181] Make step functions public (so docs show), add documentation --- macq/extract/arms.py | 77 ++++++++++++++++++++++++++++++++------------ 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 5d090be9..eebf856f 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -133,7 +133,7 @@ def _arms( early_actions = [0] * len(obs_lists) debug1 = ARMS.debug_menu("Debug step 1?") if debug else False - connected_actions, action_map = ARMS._step1(obs_lists, debug1) + connected_actions, action_map = ARMS.step1(obs_lists, debug1) if debug1: input("Press enter to continue...") @@ -148,7 +148,7 @@ def _arms( count += 1 debug2 = ARMS.debug_menu("Debug step 2?") if debug else False - constraints, relation_map = ARMS._step2( + constraints, relation_map = ARMS.step2( obs_lists, connected_actions, action_map, fluents, min_support, debug2 ) if debug2: @@ -159,7 +159,7 @@ def _arms( relation_map_rev[relation].append(fluent) debug3 = ARMS.debug_menu("Debug step 3?") if debug else False - max_sat, decode = ARMS._step3( + max_sat, decode = ARMS.step3( constraints, action_weight, info_weight, @@ -171,11 +171,11 @@ def _arms( if debug3: input("Press enter to continue...") - model = ARMS._step4(max_sat, decode) + model = ARMS.step4(max_sat, decode) debug5 = ARMS.debug_menu("Debug step 5?") if debug else False # Mutates the LearnedAction (keys) of action_map_rev - ARMS._step5( + ARMS.step5( model, list(action_map_rev.keys()), debug5, @@ -249,7 +249,7 @@ def _arms( return learned_actions @staticmethod - def _step1( + def step1( obs_lists: ObservationLists, debug: bool ) -> Tuple[ Dict[LearnedAction, Dict[LearnedAction, Set[str]]], @@ -285,15 +285,22 @@ def _step1( return connected_actions, action_map @staticmethod - def _step2( + def step2( obs_lists: ObservationLists, - connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], + connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set[str]]], action_map: Dict[Action, LearnedAction], fluents: Set[Fluent], min_support: int, debug: bool, ) -> Tuple[ARMSConstraints, Dict[Fluent, Relation]]: - """(Step 2) Generate action constraints, information constraints, and plan constraints.""" + """(Step 2) Generate action constraints, information constraints, and plan constraints. + + For the unexplained actions, build a set of information and action + constraints based on individual actions. Apply a frequent-set + mining algorithm to find the frequent sets of connected actions and + relations. Here connected means the actions and relations must share + some common parameters. + """ # Map fluents to relations # relations are fluents but with instantiated objects replaced by the object type @@ -312,17 +319,17 @@ def _step2( debuga = ARMS.debug_menu("Debug action constraints?") if debug else False - action_constraints = ARMS._step2A( + action_constraints = ARMS.step2A( connected_actions, set(relations.values()), debuga ) debugi = ARMS.debug_menu("Debug info constraints?") if debug else False - info_constraints, info_support_counts = ARMS._step2I( + info_constraints, info_support_counts = ARMS.step2I( obs_lists, relations, action_map, debugi ) debugp = ARMS.debug_menu("Debug plan constraints?") if debug else False - plan_constraints = ARMS._step2P( + plan_constraints = ARMS.step2P( obs_lists, connected_actions, action_map, @@ -342,12 +349,19 @@ def _step2( ) @staticmethod - def _step2A( + def step2A( connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], relations: Set[Relation], debug: bool, ) -> List[Or[Var]]: - """(Step 2 - Action Constraints)""" + """Action constraints. + + A1. The intersection of the precondition and add lists of all actions must be empty. + + A2. In addition, if an action’s delete list includes a relation, this relation is + in the action’s precondition list. Thus, for every action, we require that + the delete list is a subset of the precondition list. + """ if debug: print("\nBuilding action constraints...\n") @@ -413,14 +427,35 @@ def implication(a: Var, b: Var): return constraints @staticmethod - def _step2I( + def step2I( obs_lists: ObservationLists, relations: Dict[Fluent, Relation], actions: Dict[Action, LearnedAction], debug: bool, ) -> Tuple[List[Or[Var]], Dict[Or[Var], int]]: - """(Step 2 - Info Constraints)""" - + """Information constraints. + + The information constraints are used to explain why the optionally + observed intermediate states exist in a plan. The constraints thus derived are + given high priority because they need not be guessed. + Suppose we observe a relation p to be true between two actions + \(a_n\) and \(a_{n+1}\) , and \(p\), \(a_{i_1} , ... ,\) and \(a_{i_k}\) share + the same parameter types. We can represent this fact by the following clauses, + given that \(a_{i_1} , ... ,\) and \(a_{i_k}\) appear in that order. + + I1. The relation \(p\) must be generated by an action \(a_{i_k} (0 \le i_k \le n)\), + that is, \(p\) is selected to be in the add-list of \(a_{i_k}\). + \(p∈ (add_{i_1} ∪ add_{i_2} ∪ \dots ∪ add_{i_k}) \), where ∪ means logical “or”. + + I2. The last action \(a_{i_k}\) must not delete the relation p; that is, + \(p\) must not be selected to be in the delete list of \(a_{i_k}\): \(p \\not\in del_{i_k}\). + + I3. We define the weight value of a relation-action pair \((p, a)\) as the + occurrence probability of this pair in all plan examples. If the probability + of a relation-action pair is higher than the probability threshold θ , + then we set a corresponding relation constraint \(p ∈ \\text{PRECOND}_a\), which + receives a weight value equal to its prior probability. + """ if debug: print("\nBuilding information constraints...") constraints: List[Or[Var]] = [] @@ -551,7 +586,7 @@ def _apriori( return frequent_pairs @staticmethod - def _step2P( + def step2P( obs_lists: ObservationLists, connected_actions: Dict[LearnedAction, Dict[LearnedAction, Set]], action_map: Dict[Action, LearnedAction], @@ -609,7 +644,7 @@ def _step2P( return constraints @staticmethod - def _step3( + def step3( constraints: ARMSConstraints, action_weight: int, info_weight: int, @@ -677,7 +712,7 @@ def get_support_rate(count): return list(map(get_support_rate, support_counts)) @staticmethod - def _step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: + def step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: """(Step 4) Build the MAX-SAT theory.""" solver = RC2(max_sat) @@ -695,7 +730,7 @@ def _step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: return model @staticmethod - def _step5( + def step5( model: Dict[Hashable, bool], actions: List[LearnedAction], debug: bool, From 8301cb855f772e51703a86e0317bb17ffe2acf14 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Thu, 26 Aug 2021 15:17:20 -0400 Subject: [PATCH 159/181] fix model parsing --- macq/extract/amdn.py | 29 +++++++++++++---- macq/generate/pddl/random_goal_sampling.py | 4 +-- macq/observation/partial_observation.py | 2 +- tests/extract/test_amdn.py | 36 ++++++++++++---------- 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 65b9bb6d..c6c95853 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -1,7 +1,7 @@ from macq.extract.learned_action import LearnedAction from nnf.operators import implies import macq.extract as extract -from typing import Dict, List, Union, Set, Hashable +from typing import Dict, List, Optional, Union, Set, Hashable from nnf import Aux, Var, And, Or from pysat.formula import WCNF from pysat.examples.rc2 import RC2 @@ -210,8 +210,8 @@ def _build_hard_parallel_constraints(obs_lists: ObservationLists): for act in obs_lists.actions: for r in obs_lists.propositions: # for each action x proposition pair, enforce the two hard constraints with weight wmax - hard_constraints[implies(add(r, act), ~pre(r, act))] = WMAX - hard_constraints[implies(delete(r, act), pre(r, act))] = WMAX + hard_constraints[implies(add(r, act), ~pre(r, act))] = "HARD"#WMAX + hard_constraints[implies(delete(r, act), pre(r, act))] = "HARD"#WMAX return hard_constraints @staticmethod @@ -252,7 +252,7 @@ def _build_soft_parallel_constraints(obs_lists: ObservationLists): return soft_constraints @staticmethod - def _build_parallel_constraints(obs_lists: ObservationLists, debug: int, to_obs: List[str]): + def _build_parallel_constraints(obs_lists: ObservationLists, debug: int, to_obs: Optional[List[str]]): hard_constraints = AMDN._build_hard_parallel_constraints(obs_lists) soft_constraints = AMDN._build_soft_parallel_constraints(obs_lists) if debug: @@ -362,7 +362,7 @@ def _noise_constraints_8(obs_lists, all_occ: int, occ_threshold: int): return noise_constraints_8 @staticmethod - def _build_noise_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int, to_obs: List[str]): + def _build_noise_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int, to_obs: Optional[List[str]]): # calculate all occurrences for use in weights all_occ = AMDN._calculate_all_r_occ(obs_lists) nc_6 = AMDN._noise_constraints_6(obs_lists, all_occ, occ_threshold) @@ -379,6 +379,7 @@ def _build_noise_constraints(obs_lists: ObservationLists, occ_threshold: int, de @staticmethod def _set_all_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int): + to_obs = None if debug: to_obs = AMDN._get_observe(obs_lists) disorder_constraints = AMDN._build_disorder_constraints(obs_lists) @@ -392,6 +393,7 @@ def _set_all_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: @staticmethod def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int): constraints = AMDN._set_all_constraints(obs_lists, occ_threshold, debug) + # extract hard constraints hard_constraints = [] for c, weight in constraints.items(): @@ -401,6 +403,21 @@ def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: i for c in hard_constraints: del constraints[c] + # NOTE: just a test + r = list(obs_lists.propositions)[0] + act = list(obs_lists.actions)[0] + + """ + # hard parallel constraints 1: successfully fails the model + + hard_constraints.append(AMDN._or_refactor(add(r, act))) + hard_constraints.append(AMDN._or_refactor(pre(r, act))) + + # hard parallel constraints 2: successfully fails the model + hard_constraints.append(AMDN._or_refactor(delete(r, act))) + hard_constraints.append(AMDN._or_refactor(~pre(r, act))) + """ + wcnf, decode = to_wcnf(soft_clauses=And(constraints.keys()), hard_clauses=And(hard_constraints), weights=list(constraints.values())) return wcnf, decode @@ -449,7 +466,7 @@ def _extract_model(obs_lists: ObservationLists, model: Dict[Hashable, bool]): # iterate through all fluents for raw_f in model: # update learned_actions (ignore auxiliary variables) - if not isinstance(raw_f, Aux): + if not isinstance(raw_f, Aux) and model[raw_f]: AMDN._split_raw_fluent(raw_f, learned_actions) return Model(fluents, learned_actions.values()) \ No newline at end of file diff --git a/macq/generate/pddl/random_goal_sampling.py b/macq/generate/pddl/random_goal_sampling.py index 17ea53e2..47cf4c77 100644 --- a/macq/generate/pddl/random_goal_sampling.py +++ b/macq/generate/pddl/random_goal_sampling.py @@ -9,7 +9,7 @@ -MAX_GOAL_SEARCH_TIME = 30.0 +MAX_GOAL_SEARCH_TIME = 0.0 class RandomGoalSampling(VanillaSampling): @@ -155,7 +155,7 @@ def generate_goals(self, goal_states: Dict): if len(test_plan.actions) >= self.steps_deep: k_length_plans += 1 if k_length_plans >= self.num_traces: - break + break def generate_traces(self): """Generates traces based on the sampled goals. Traces are generated using the initial state and plan used to achieve the goal. diff --git a/macq/observation/partial_observation.py b/macq/observation/partial_observation.py index 4b37b812..a6838699 100644 --- a/macq/observation/partial_observation.py +++ b/macq/observation/partial_observation.py @@ -32,7 +32,7 @@ def __init__( raise PercentError() if percent_missing == 0 and not hide: - warning("Creating a PartialObseration with no missing information.") + warning("Creating a PartialObservation with no missing information.") # necessary because multiple inheritance can change the parent of this class Observation.__init__(self, index=step.index) diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 911d0231..f7b6994a 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -2,7 +2,7 @@ from macq.utils.tokenization_errors import TokenizationError from tests.utils.generators import generate_blocks_traces from macq.extract import Extract, modes -from macq.generate.pddl import RandomGoalSampling, Generator, TraceFromGoal +from macq.generate.pddl import * from macq.observation import * from macq.trace import * from pathlib import Path @@ -161,29 +161,31 @@ def test_tracelist(): if __name__ == "__main__": # exit out to the base macq folder so we can get to /tests base = Path(__file__).parent.parent - """ + dom = str((base / "pddl_testing_files/blocks_domain.pddl").resolve()) prob = str((base / "pddl_testing_files/blocks_problem.pddl").resolve()) # TODO: replace with a domain-specific random trace generator - traces = RandomGoalSampling( - # prob=prob, - # dom=dom, - problem_id=2337, + #traces = RandomGoalSampling( + traces = VanillaSampling( + prob=prob, + dom=dom, + #problem_id=2337, observe_pres_effs=True, - num_traces=1, - steps_deep=50, - subset_size_perc=0.1, - enforced_hill_climbing_sampling=True + num_traces=10, + plan_len=10, + #steps_deep=30, + #subset_size_perc=0.1, + #enforced_hill_climbing_sampling=True ).traces - traces.print() - traces = test_tracelist() - """ + traces.print() + + #traces = test_tracelist() - dom = str((base / "pddl_testing_files/door_dom.pddl").resolve()) - prob = str((base / "pddl_testing_files/door_prob.pddl").resolve()) - traces = TraceList([TraceFromGoal(dom=dom, prob=prob, observe_pres_effs=True).trace]) + # dom = str((base / "pddl_testing_files/door_dom.pddl").resolve()) + # prob = str((base / "pddl_testing_files/door_prob.pddl").resolve()) + # traces = TraceList([TraceFromGoal(dom=dom, prob=prob, observe_pres_effs=True).trace]) features = [objects_shared_feature, num_parameters_feature] learned_theta = default_theta_vec(2) @@ -196,6 +198,6 @@ def test_tracelist(): percent_noisy=0, replace=True ) - model = Extract(observations, modes.AMDN, debug=True, occ_threshold = 0) + model = Extract(observations, modes.AMDN, debug=False, occ_threshold = 10) f = open("results.txt", "w") f.write(model.details()) From e1402b68c2140f0412ebcd83a3dc93ecb21581d2 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 26 Aug 2021 17:17:27 -0400 Subject: [PATCH 160/181] Add documentation for remaining steps --- macq/extract/__init__.py | 8 +++--- macq/extract/arms.py | 59 ++++++++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/macq/extract/__init__.py b/macq/extract/__init__.py index 9995f451..d5559caf 100644 --- a/macq/extract/__init__.py +++ b/macq/extract/__init__.py @@ -1,16 +1,16 @@ +from .learned_fluent import LearnedFluent +from .learned_action import LearnedAction from .model import Model, LearnedAction from .extract import Extract, modes from .exceptions import IncompatibleObservationToken from .model import Model from .extract import Extract, modes -from .learned_fluent import LearnedFluent -from .learned_action import LearnedAction __all__ = [ + "LearnedAction", + "LearnedFluent", "Model", "Extract", "modes", "IncompatibleObservationToken", - "LearnedAction", - "LearnedFluent", ] diff --git a/macq/extract/arms.py b/macq/extract/arms.py index eebf856f..19dd3a01 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -3,7 +3,7 @@ from warnings import warn from typing import Set, List, Dict, Tuple, Hashable from nnf import Var, And, Or, false as nnffalse -from . import LearnedAction, Model +from . import LearnedAction, Model, LearnedFluent from .exceptions import ( IncompatibleObservationToken, InvalidMaxSATModel, @@ -15,13 +15,17 @@ @dataclass class Relation: + """Fluents with the parameters replaced by their types.""" + name: str types: list def var(self): + """Generates the variable representation for NNF.""" return f"{self.name} {' '.join(list(self.types))}" def matches(self, action: LearnedAction): + """Determines if a relation is related to a given action.""" action_types = set(action.obj_params) self_counts = Counter(self.types) action_counts = Counter(action.obj_params) @@ -35,6 +39,8 @@ def __hash__(self): @dataclass class ARMSConstraints: + """A dataclass to hold all the constraints and weight information.""" + action: List[Or[Var]] info: List[Or[Var]] info3: Dict[Or[Var], int] @@ -96,8 +102,8 @@ def __new__( if not (threshold >= 0 and threshold <= 1): raise ARMS.InvalidThreshold(threshold) - # get fluents from initial state fluents = obs_lists.get_fluents() + # get fluents from initial state # call algorithm to get actions actions = ARMS._arms( obs_lists, @@ -112,7 +118,8 @@ def __new__( debug, ) - return Model(fluents, actions) + learned_fluents = set(map(lambda f: LearnedFluent(f.name, f.objects), fluents)) + return Model(learned_fluents, actions) @staticmethod def _arms( @@ -435,11 +442,8 @@ def step2I( ) -> Tuple[List[Or[Var]], Dict[Or[Var], int]]: """Information constraints. - The information constraints are used to explain why the optionally - observed intermediate states exist in a plan. The constraints thus derived are - given high priority because they need not be guessed. - Suppose we observe a relation p to be true between two actions - \(a_n\) and \(a_{n+1}\) , and \(p\), \(a_{i_1} , ... ,\) and \(a_{i_k}\) share + Suppose we observe a relation p to be true between two actions + \(a_n\) and \(a_{n+1}\) , and \(p, a_{i_1} , ... ,\) and \(a_{i_k}\) share the same parameter types. We can represent this fact by the following clauses, given that \(a_{i_1} , ... ,\) and \(a_{i_k}\) appear in that order. @@ -594,7 +598,40 @@ def step2P( min_support: int, debug: bool, ) -> Dict[Or[Var], int]: - """(Step 2 - Plan Constraints)""" + """Plan constraints. + + P1. Every precondition \(p\) of every action \(b\) must be in the add + list of a preceding action \(a\) and is not deleted by any actions between + \(a\) and \(b\). + + P2. In addition, at least one relation \(r\) in the add list of an action + must be useful in achieving a precondition of a later action. That is, for + every action \(a\), an add list relation \(r\) must be in the precondition of a + later action \(b\), and there is no other action between \(a\) and \(b\) + that either adds or deletes \(r\). + + "While constraints P1 and P2 provide the general guiding principle for + ensuring a plan’s correctness, in practice there are too many + instantiations of these constraints." (more information on the + rationelle in the paper) Thus, they are replaced with the following + constraints: + + Let there be an action pair \(\langle a_i, a_j \\rangle, 0 \le i < j \le n-1\). + + P3. One of the relevant relations \(p\) must be chosen to be in the + preconditions of both \(a_i\) and \(a_j\), but not in the delete list + of \(a_i\). + + P4. The first action \(a_i\) adds a relevant relation that is in the + precondition list of the second action \(a_j\) in the pair. + + P5. A relevant relation \(p\) that is deleted by the first action + \(a_i\) is added by \(a_j\). The second clause is designed for the event + when an action re-establishes a fact that is deleted by a previous action. + + The above constraints can be combined into one constraint: + $$\exists p ((p \in (pre_i \cap pre_j) \land p \\not \in (del_i)) \lor (p \in (add_i \cap pre_j)) \lor (p \in (del_i \cap add_j)))$$ + """ frequent_pairs = ARMS._apriori( [ [ @@ -653,7 +690,7 @@ def step3( plan_default: int, debug: bool, ) -> Tuple[WCNF, Dict[int, Hashable]]: - """(Step 3) Construct the weighted MAX-SAT problem.""" + """(Step 3) Construct the weighted MAX-SAT problem based on the constraints and weight information found in Step 2.""" action_weights = [action_weight] * len(constraints.action) info_weights = [info_weight] * len(constraints.info) @@ -713,7 +750,7 @@ def get_support_rate(count): @staticmethod def step4(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: - """(Step 4) Build the MAX-SAT theory.""" + """(Step 4) Solve the MAX-SAT problem built in Step 3.""" solver = RC2(max_sat) encoded_model = solver.compute() From 8a797c5f0d5b3309deaf4f5daee1942e8af49e8e Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 26 Aug 2021 17:41:49 -0400 Subject: [PATCH 161/181] Add ARMS usage docs --- docs/extract/arms.md | 23 ++++++++++++++++++++++- macq/extract/amdn.py | 4 +--- macq/extract/arms.py | 2 ++ macq/extract/observer.py | 4 +--- macq/extract/slaf.py | 5 ++--- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/docs/extract/arms.md b/docs/extract/arms.md index 8de088fe..aa4f7a61 100644 --- a/docs/extract/arms.md +++ b/docs/extract/arms.md @@ -46,11 +46,32 @@ model = extract.Extract( info3_default=30, plan_default=30, ) -model.details() + +print(model.details()) ``` **Output:** ```text +Model: + Fluents: (at rover rover0 waypoint waypoint2), (have_soil_analysis rover rover0 waypoint waypoint2), (have_soil_analysis rover rover0 waypoint waypoint3), ... + Actions: + (communicate_image_data rover waypoint mode objective lander waypoint): + precond: + calibrated camera rover + have_rock_analysis rover waypoint + communicated_rock_data waypoint + channel_free lander + at_soil_sample waypoint + at_rock_sample waypoint + add: + calibrated camera rover + at rover waypoint + have_image rover objective mode + channel_free lander + communicated_image_data objective mode + delete: + calibrated camera rover + ... ``` # API Documentation diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index d5045027..b4d64e3e 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -1,6 +1,4 @@ -""" -.. include:: ../../docs/extract/amdn.md -""" +""".. include:: ../../docs/extract/amdn.md""" import macq.extract as extract from .model import Model diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 19dd3a01..08d27987 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -1,3 +1,5 @@ +""".. include:: ../../docs/extract/arms.md""" + from collections import defaultdict, Counter from dataclasses import dataclass from warnings import warn diff --git a/macq/extract/observer.py b/macq/extract/observer.py index 7060a102..f881fbb5 100644 --- a/macq/extract/observer.py +++ b/macq/extract/observer.py @@ -1,6 +1,4 @@ -""" -.. include:: ../../docs/extract/observer.md -""" +""".. include:: ../../docs/extract/observer.md""" from typing import List, Set from collections import defaultdict diff --git a/macq/extract/slaf.py b/macq/extract/slaf.py index d3e31a64..08011e2b 100644 --- a/macq/extract/slaf.py +++ b/macq/extract/slaf.py @@ -1,6 +1,5 @@ -""" -.. include:: ../../docs/extract/slaf.md -""" +""".. include:: ../../docs/extract/slaf.md""" + import macq.extract as extract from typing import Set, Union from nnf import Var, Or, And, true, false, config From 350ae82583444296501c0d052321288b4a395f06 Mon Sep 17 00:00:00 2001 From: ecal Date: Thu, 26 Aug 2021 17:46:56 -0400 Subject: [PATCH 162/181] Add Extract documentation --- docs/extract/extract.md | 9 +++++++++ macq/extract/extract.py | 2 ++ 2 files changed, 11 insertions(+) create mode 100644 docs/extract/extract.md diff --git a/docs/extract/extract.md b/docs/extract/extract.md new file mode 100644 index 00000000..88b7e77e --- /dev/null +++ b/docs/extract/extract.md @@ -0,0 +1,9 @@ +# Usage + +## Debugging +Include the argument `debug=True` to `Extract` to enable debugging for any +extraction technique. + +*Note: debugging output and interfaces are unique to each method.* + +# API Documentation diff --git a/macq/extract/extract.py b/macq/extract/extract.py index 39def5bd..e148e296 100644 --- a/macq/extract/extract.py +++ b/macq/extract/extract.py @@ -1,3 +1,5 @@ +""".. include:: ../../docs/extract/extract.md""" + from dataclasses import dataclass from enum import Enum, auto from ..trace import Action, State From 6f08ec5cd017794de8a6622d82206d50fdc154a5 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Fri, 27 Aug 2021 11:29:28 -0400 Subject: [PATCH 163/181] attempt to debug door domain --- macq/extract/amdn.py | 54 ++++++++++++------- ...ered_parallel_actions_observation_lists.py | 1 + tests/extract/test_amdn.py | 34 ++++++------ 3 files changed, 53 insertions(+), 36 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index c6c95853..6af19907 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -107,12 +107,11 @@ def _get_observe(obs_lists: ObservationLists): @staticmethod def _debug_is_observed(constraint: Or, to_obs: List[str]): - observe = False for c in constraint.children: for v in to_obs: if v in str(c): - observe = True - return observe + return True + return False @staticmethod def _debug_simple_pprint(constraints: Dict, to_obs: List[str]): @@ -127,8 +126,7 @@ def _debug_aux_pprint(constraints: Dict, to_obs: List[str]): aux_map = {} index = 0 for c in constraints: - observe = AMDN._debug_is_observed(c, to_obs) - if observe: + if AMDN._debug_is_observed(c, to_obs): for var in c.children: if isinstance(var.name, Aux) and var.name not in aux_map: aux_map[var.name] = f"aux {index}" @@ -136,13 +134,13 @@ def _debug_aux_pprint(constraints: Dict, to_obs: List[str]): all_pretty_c = {} for c in constraints: - observe = AMDN._debug_is_observed(c, to_obs) - if observe: + if AMDN._debug_is_observed(c, to_obs): pretty_c = [] for var in c.children: if isinstance(var.name, Aux): if var.true: pretty_c.append(Var(aux_map[var.name])) + all_pretty_c[AMDN._or_refactor(var)] = Or([Var(aux_map[var.name])]) else: pretty_c.append(~Var(aux_map[var.name])) else: @@ -389,6 +387,7 @@ def _set_all_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: parallel_constraints = AMDN._build_parallel_constraints(obs_lists, debug, to_obs) noise_constraints = AMDN._build_noise_constraints(obs_lists, occ_threshold, debug, to_obs) return {**disorder_constraints, **parallel_constraints, **noise_constraints} + #return {**parallel_constraints, **noise_constraints} @staticmethod def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int): @@ -404,19 +403,36 @@ def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: i del constraints[c] # NOTE: just a test - r = list(obs_lists.propositions)[0] - act = list(obs_lists.actions)[0] - - """ - # hard parallel constraints 1: successfully fails the model - - hard_constraints.append(AMDN._or_refactor(add(r, act))) - hard_constraints.append(AMDN._or_refactor(pre(r, act))) + # r = list(obs_lists.propositions)[0] + # act = list(obs_lists.actions)[0] - # hard parallel constraints 2: successfully fails the model - hard_constraints.append(AMDN._or_refactor(delete(r, act))) - hard_constraints.append(AMDN._or_refactor(~pre(r, act))) - """ + # hard parallel constraints 1: successfully fails the model, but ONLY if WMAX is not used and they are set as direct hard constraints + # hard_constraints.append(AMDN._or_refactor(add(r, act))) + # hard_constraints.append(AMDN._or_refactor(pre(r, act))) + # hard parallel constraints 2: successfully fails the model, but ONLY if WMAX is not used and they are set as direct hard constraints + #hard_constraints.append(AMDN._or_refactor(delete(r, act))) + #hard_constraints.append(AMDN._or_refactor(~pre(r, act))) + + #act_x = list(list(obs_lists.all_par_act_sets[0])[0])[0] + #act_y = list(list(obs_lists.all_par_act_sets[0])[1])[0] + + # attempt to force the model + hard_constraints.append(AMDN._or_refactor(Var("(rooma is a precondition of open )"))) + hard_constraints.append(AMDN._or_refactor(Var("(open is added by open )"))) + hard_constraints.append(AMDN._or_refactor(Var("(rooma is a precondition of walk )"))) + hard_constraints.append(AMDN._or_refactor(Var("(open is a precondition of walk )"))) + hard_constraints.append(AMDN._or_refactor(Var("(rooma is deleted by walk )"))) + hard_constraints.append(AMDN._or_refactor(Var("(roomb is added by walk )"))) + + delete = set() + # set all "ordered" aux constraints to hard + for c in constraints: + if len(c.children) == 1: + if constraints[c] > 0: + hard_constraints.append(c) + delete.add(c) + for c in delete: + del constraints[c] wcnf, decode = to_wcnf(soft_clauses=And(constraints.keys()), hard_clauses=And(hard_constraints), weights=list(constraints.values())) diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index 61a28b7e..d36083d1 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -284,6 +284,7 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): par_act_sets[i].add(act_y) par_act_sets[j].discard(act_y) par_act_sets[j].add(act_x) + self.all_par_act_sets.append(par_act_sets) self.all_states.append(states) tokens = [] diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index f7b6994a..4d5b2846 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -167,25 +167,25 @@ def test_tracelist(): # TODO: replace with a domain-specific random trace generator #traces = RandomGoalSampling( - traces = VanillaSampling( - prob=prob, - dom=dom, - #problem_id=2337, - observe_pres_effs=True, - num_traces=10, - plan_len=10, - #steps_deep=30, - #subset_size_perc=0.1, - #enforced_hill_climbing_sampling=True - ).traces - - traces.print() + # traces = VanillaSampling( + # prob=prob, + # dom=dom, + # #problem_id=2337, + # observe_pres_effs=True, + # num_traces=1, + # plan_len=10, + # #steps_deep=30, + # #subset_size_perc=0.1, + # #enforced_hill_climbing_sampling=True + # ).traces #traces = test_tracelist() - # dom = str((base / "pddl_testing_files/door_dom.pddl").resolve()) - # prob = str((base / "pddl_testing_files/door_prob.pddl").resolve()) - # traces = TraceList([TraceFromGoal(dom=dom, prob=prob, observe_pres_effs=True).trace]) + dom = str((base / "pddl_testing_files/door_dom.pddl").resolve()) + prob = str((base / "pddl_testing_files/door_prob.pddl").resolve()) + traces = TraceList([TraceFromGoal(dom=dom, prob=prob, observe_pres_effs=True).trace]) + + traces.print(wrap="y") features = [objects_shared_feature, num_parameters_feature] learned_theta = default_theta_vec(2) @@ -198,6 +198,6 @@ def test_tracelist(): percent_noisy=0, replace=True ) - model = Extract(observations, modes.AMDN, debug=False, occ_threshold = 10) + model = Extract(observations, modes.AMDN, debug=False, occ_threshold = 2) f = open("results.txt", "w") f.write(model.details()) From ff130da374c50e63747e6e9d221911fa19d28482 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Fri, 27 Aug 2021 14:24:11 -0400 Subject: [PATCH 164/181] truck domain debug --- macq/extract/amdn.py | 36 ++++++++++++++-------------- tests/extract/test_amdn.py | 49 +++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 6af19907..74ff3586 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -208,8 +208,8 @@ def _build_hard_parallel_constraints(obs_lists: ObservationLists): for act in obs_lists.actions: for r in obs_lists.propositions: # for each action x proposition pair, enforce the two hard constraints with weight wmax - hard_constraints[implies(add(r, act), ~pre(r, act))] = "HARD"#WMAX - hard_constraints[implies(delete(r, act), pre(r, act))] = "HARD"#WMAX + hard_constraints[implies(add(r, act), ~pre(r, act))] = WMAX + hard_constraints[implies(delete(r, act), pre(r, act))] = WMAX return hard_constraints @staticmethod @@ -417,22 +417,22 @@ def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: i #act_y = list(list(obs_lists.all_par_act_sets[0])[1])[0] # attempt to force the model - hard_constraints.append(AMDN._or_refactor(Var("(rooma is a precondition of open )"))) - hard_constraints.append(AMDN._or_refactor(Var("(open is added by open )"))) - hard_constraints.append(AMDN._or_refactor(Var("(rooma is a precondition of walk )"))) - hard_constraints.append(AMDN._or_refactor(Var("(open is a precondition of walk )"))) - hard_constraints.append(AMDN._or_refactor(Var("(rooma is deleted by walk )"))) - hard_constraints.append(AMDN._or_refactor(Var("(roomb is added by walk )"))) - - delete = set() - # set all "ordered" aux constraints to hard - for c in constraints: - if len(c.children) == 1: - if constraints[c] > 0: - hard_constraints.append(c) - delete.add(c) - for c in delete: - del constraints[c] + # hard_constraints.append(AMDN._or_refactor(Var("(rooma is a precondition of open )"))) + # hard_constraints.append(AMDN._or_refactor(Var("(open is added by open )"))) + # hard_constraints.append(AMDN._or_refactor(Var("(rooma is a precondition of walk )"))) + # hard_constraints.append(AMDN._or_refactor(Var("(open is a precondition of walk )"))) + # hard_constraints.append(AMDN._or_refactor(Var("(rooma is deleted by walk )"))) + # hard_constraints.append(AMDN._or_refactor(Var("(roomb is added by walk )"))) + + # hard_constraints.append(AMDN._or_refactor(Var("(truck red_truck is a precondition of drive red_truck location_a location_b)"))) + # hard_constraints.append(AMDN._or_refactor(Var("(place location_a is a precondition of drive red_truck location_a location_b)"))) + # hard_constraints.append(AMDN._or_refactor(Var("(place location_b is a precondition of drive red_truck location_a location_b)"))) + # hard_constraints.append(AMDN._or_refactor(Var("(at red_truck location_a is a precondition of drive red_truck location_a location_b)"))) + # TODO: add add/del effects if you're going to use these + # hard_constraints.append(AMDN._or_refactor(Var("(truck blue_truck is a precondition of drive blue_truck location_c location_d)"))) + # hard_constraints.append(AMDN._or_refactor(Var("(place location_c is a precondition of drive blue_truck location_c location_d)"))) + # hard_constraints.append(AMDN._or_refactor(Var("(place location_d is a precondition of drive blue_truck location_c location_d)"))) + # hard_constraints.append(AMDN._or_refactor(Var("(at blue_truck location_c is a precondition of drive blue_truck location_c location_d)"))) wcnf, decode = to_wcnf(soft_clauses=And(constraints.keys()), hard_clauses=And(hard_constraints), weights=list(constraints.values())) diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 4d5b2846..f49dd747 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -15,12 +15,12 @@ def test_tokenization_error(): def test_tracelist(): # define objects - red_truck = PlanningObject("object", "red_truck") - blue_truck = PlanningObject("object", "blue_truck") - location_a = PlanningObject("object", "location_a") - location_b = PlanningObject("object", "location_b") - location_c = PlanningObject("object", "location_c") - location_d = PlanningObject("object", "location_d") + red_truck = PlanningObject("", "red_truck") + blue_truck = PlanningObject("", "blue_truck") + location_a = PlanningObject("", "location_a") + location_b = PlanningObject("", "location_b") + location_c = PlanningObject("", "location_c") + location_d = PlanningObject("", "location_d") red_truck_is_truck = Fluent("truck", [red_truck]) blue_truck_is_truck = Fluent("truck", [blue_truck]) @@ -154,8 +154,9 @@ def test_tracelist(): }), None, 4 - ) - + ) + #step_2.action = None + #return TraceList([Trace([step_0, step_1, step_2])])#, step_3, step_4])]) return TraceList([Trace([step_0, step_1, step_2, step_3, step_4])]) if __name__ == "__main__": @@ -166,24 +167,24 @@ def test_tracelist(): prob = str((base / "pddl_testing_files/blocks_problem.pddl").resolve()) # TODO: replace with a domain-specific random trace generator - #traces = RandomGoalSampling( + traces = RandomGoalSampling( # traces = VanillaSampling( - # prob=prob, - # dom=dom, - # #problem_id=2337, - # observe_pres_effs=True, - # num_traces=1, - # plan_len=10, - # #steps_deep=30, - # #subset_size_perc=0.1, - # #enforced_hill_climbing_sampling=True - # ).traces + prob=prob, + dom=dom, + #problem_id=2337, + observe_pres_effs=True, + num_traces=1, + # plan_len=10, + steps_deep=30, + subset_size_perc=0.1, + enforced_hill_climbing_sampling=True + ).traces - #traces = test_tracelist() + # traces = test_tracelist() - dom = str((base / "pddl_testing_files/door_dom.pddl").resolve()) - prob = str((base / "pddl_testing_files/door_prob.pddl").resolve()) - traces = TraceList([TraceFromGoal(dom=dom, prob=prob, observe_pres_effs=True).trace]) + # dom = str((base / "pddl_testing_files/door_dom.pddl").resolve()) + # prob = str((base / "pddl_testing_files/door_prob.pddl").resolve()) + # traces = TraceList([TraceFromGoal(dom=dom, prob=prob, observe_pres_effs=True).trace]) traces.print(wrap="y") @@ -198,6 +199,6 @@ def test_tracelist(): percent_noisy=0, replace=True ) - model = Extract(observations, modes.AMDN, debug=False, occ_threshold = 2) + model = Extract(observations, modes.AMDN, debug=True, occ_threshold = 2) f = open("results.txt", "w") f.write(model.details()) From 4630832ba99fd438c09a56e782c9186e3bff26b5 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Mon, 30 Aug 2021 14:31:37 -0400 Subject: [PATCH 165/181] add wrappers to add timer parameters --- macq/generate/pddl/random_goal_sampling.py | 140 +++++++++--------- macq/generate/pddl/vanilla_sampling.py | 100 +++++++------ .../pddl/test_random_goal_sampling.py | 7 +- tests/generate/pddl/test_vanilla_sampling.py | 9 +- 4 files changed, 137 insertions(+), 119 deletions(-) diff --git a/macq/generate/pddl/random_goal_sampling.py b/macq/generate/pddl/random_goal_sampling.py index 054dde66..27a575b2 100644 --- a/macq/generate/pddl/random_goal_sampling.py +++ b/macq/generate/pddl/random_goal_sampling.py @@ -41,6 +41,7 @@ def __init__( dom: str = None, prob: str = None, problem_id: int = None, + max_time: float = 30 ): """ Initializes a random goal state trace sampler using the plan length, number of traces, @@ -70,7 +71,7 @@ def __init__( self.enforced_hill_climbing_sampling = enforced_hill_climbing_sampling self.subset_size_perc = subset_size_perc self.goals_inits_plans = [] - super().__init__(dom=dom, prob=prob, problem_id=problem_id, num_traces=num_traces) + super().__init__(dom=dom, prob=prob, problem_id=problem_id, num_traces=num_traces, max_time=max_time) def goal_sampling(self): """Samples goals by randomly generating candidate goal states k (`steps_deep`) steps deep, then running planners on those @@ -81,7 +82,7 @@ def goal_sampling(self): Returns: An OrderedDict holding the longest goal states along with the initial state and plans used to reach them. """ goal_states = {} - self.generate_goals(goal_states=goal_states) + self.generate_goals_setup(num_seconds=self.max_time, goal_states=goal_states)() # sort the results by plan length and get the k largest ones filtered_goals = OrderedDict(sorted(goal_states.items(), key=lambda x : len(x[1]["plan"].actions))) to_del = list(filtered_goals.keys())[:len(filtered_goals) - self.num_traces] @@ -89,70 +90,77 @@ def goal_sampling(self): del filtered_goals[d] return filtered_goals - @basic_timer(num_seconds=MAX_GOAL_SEARCH_TIME) - def generate_goals(self, goal_states: Dict): - """Helper function for `goal_sampling`. Generates as many goals as possible within MAX_GOAL_SEARCH_TIME seconds. - Given the specified number of traces `num_traces`, if `num_traces` plans of length k (`steps_deep`) are found before - the time is up, exit early. - - Args: - goal_states (Dict): - The dictionary to fill with the values of each goal state, initial state, and plan. - """ - # create a sampler to test the complexity of the new goal by running a planner on it - k_length_plans = 0 - while True: - # generate a trace of the specified length and retrieve the state of the last step - state = self.generate_single_trace(self.steps_deep)[-1].state - - # get all positive fluents (only positive fluents can be used for a goal) - goal_f = [f for f in state if state[f]] - # get next initial state (only used for enforced hill climbing sampling) - next_init_f = goal_f.copy() - # get the subset size - subset_size = int(len(state.fluents) * self.subset_size_perc) - # if necessary, take a subset of the fluents - if len(goal_f) > subset_size: - random.shuffle(goal_f) - goal_f = goal_f[:subset_size] - - self.change_goal(goal_fluents=goal_f) - - # ensure that the goal doesn't hold in the initial state; restart if it does - init_state = { - str(a) for a in self.problem.init.as_atoms() - } - goal = { - str(a) for a in self.problem.goal.subformulas - } - - if goal.issubset(init_state): - continue - - try: - # attempt to generate a plan, and find a new goal if a plan can't be found - # should only crash if there are server issues - test_plan = self.generate_plan() - except KeyError as e: - continue - - # create a State and add it to the dictionary - state_dict = {} - for f in goal_f: - state_dict[f] = True - # map each goal to the initial state and plan used to achieve it - goal_states[State(state_dict)] = {"plan": test_plan, "initial state": self.problem.init} - - # optionally change the initial state of the sampler for the next iteration to the goal state just generated (ensures more diversity in goals/plans) - # use the full state the goal was extracted from as the initial state to prevent planning errors from incomplete initial states - if self.enforced_hill_climbing_sampling: - self.change_init(next_init_f) - - # keep track of the number of plans of length k; if we get enough of them, exit early - if len(test_plan.actions) >= self.steps_deep: - k_length_plans += 1 - if k_length_plans >= self.num_traces: - break + def generate_goals_setup(self, num_seconds: float, goal_states: Dict): + @basic_timer(num_seconds=num_seconds) + def generate_goals(self=self, goal_states=goal_states): + """Helper function for `goal_sampling`. Generates as many goals as possible within the specified max_time seconds (timing is + enforced by the basic_timer wrapper). + + The outside function is a wrapper that provides parameters for both the timer + wrapper and the function. + + Given the specified number of traces `num_traces`, if `num_traces` plans of length k (`steps_deep`) are found before + the time is up, exit early. + + Args: + goal_states (Dict): + The dictionary to fill with the values of each goal state, initial state, and plan. + """ + # create a sampler to test the complexity of the new goal by running a planner on it + k_length_plans = 0 + while True: + # generate a trace of the specified length and retrieve the state of the last step + state = self.generate_single_trace_setup(num_seconds, self.steps_deep)()[-1].state + + # get all positive fluents (only positive fluents can be used for a goal) + goal_f = [f for f in state if state[f]] + # get next initial state (only used for enforced hill climbing sampling) + next_init_f = goal_f.copy() + # get the subset size + subset_size = int(len(state.fluents) * self.subset_size_perc) + # if necessary, take a subset of the fluents + if len(goal_f) > subset_size: + random.shuffle(goal_f) + goal_f = goal_f[:subset_size] + + self.change_goal(goal_fluents=goal_f) + + # ensure that the goal doesn't hold in the initial state; restart if it does + init_state = { + str(a) for a in self.problem.init.as_atoms() + } + goal = { + str(a) for a in self.problem.goal.subformulas + } + + if goal.issubset(init_state): + continue + + try: + # attempt to generate a plan, and find a new goal if a plan can't be found + # should only crash if there are server issues + test_plan = self.generate_plan() + except KeyError: + continue + + # create a State and add it to the dictionary + state_dict = {} + for f in goal_f: + state_dict[f] = True + # map each goal to the initial state and plan used to achieve it + goal_states[State(state_dict)] = {"plan": test_plan, "initial state": self.problem.init} + + # optionally change the initial state of the sampler for the next iteration to the goal state just generated (ensures more diversity in goals/plans) + # use the full state the goal was extracted from as the initial state to prevent planning errors from incomplete initial states + if self.enforced_hill_climbing_sampling: + self.change_init(next_init_f) + + # keep track of the number of plans of length k; if we get enough of them, exit early + if len(test_plan.actions) >= self.steps_deep: + k_length_plans += 1 + if k_length_plans >= self.num_traces: + break + return generate_goals def generate_traces(self): """Generates traces based on the sampled goals. Traces are generated using the initial state and plan used to achieve the goal. diff --git a/macq/generate/pddl/vanilla_sampling.py b/macq/generate/pddl/vanilla_sampling.py index 37bf5b98..1cef7f0d 100644 --- a/macq/generate/pddl/vanilla_sampling.py +++ b/macq/generate/pddl/vanilla_sampling.py @@ -5,15 +5,11 @@ from ...observation.partial_observation import PercentError from ...trace import ( Step, - State, Trace, TraceList, ) -MAX_TRACE_TIME = 30.0 - - class VanillaSampling(Generator): """Vanilla State Trace Sampler - inherits the base Generator class and its attributes. @@ -37,6 +33,7 @@ def __init__( prob: str = None, problem_id: int = None, seed: int = None, + max_time: float = 30 ): """ Initializes a vanilla state trace sampler using the plan length, number of traces, @@ -55,6 +52,7 @@ def __init__( The ID of the problem to access. """ super().__init__(dom=dom, prob=prob, problem_id=problem_id) + self.max_time = max_time self.plan_len = set_plan_length(plan_len) self.num_traces = set_num_traces(num_traces) self.traces = self.generate_traces() @@ -69,52 +67,56 @@ def generate_traces(self): A TraceList object with the list of traces generated. """ traces = TraceList() - traces.generator = self.generate_single_trace + traces.generator = self.generate_single_trace_setup(num_seconds=self.max_time, plan_len=self.plan_len) for _ in range(self.num_traces): - traces.append(self.generate_single_trace()) + traces.append(traces.generator()) return traces - @set_timer_throw_exc(num_seconds=MAX_TRACE_TIME, exception=TraceSearchTimeOut) - def generate_single_trace(self, plan_len: int = None): - """Generates a single trace using the uniform random sampling technique. - Loops until a valid trace is found. Wrapper does not allow the function - to run past the time specified. - - Returns: - A Trace object (the valid trace generated). - """ - - if not plan_len: - plan_len = self.plan_len - - trace = Trace() - - state = self.problem.init - valid_trace = False - while not valid_trace: - trace.clear() - # add more steps while the trace has not yet reached the desired length - for j in range(plan_len): - # if we have not yet reached the last step - if len(trace) < plan_len - 1: - # find the next applicable actions - app_act = list(self.instance.applicable(state)) - # if the trace reaches a dead lock, disregard this trace and try again - if not app_act: - break - # pick a random applicable action and apply it - act = random.choice(app_act) - # create the trace and progress the state - macq_action = self.tarski_act_to_macq(act) - macq_state = self.tarski_state_to_macq(state) - step = Step(macq_state, macq_action, j + 1) - trace.append(step) - state = progress(state, act) - else: - macq_state = self.tarski_state_to_macq(state) - step = Step(state=macq_state, action=None, index=j + 1) - trace.append(step) - valid_trace = True - return trace - + def generate_single_trace_setup(self, num_seconds: float, plan_len: int = None): + @set_timer_throw_exc(num_seconds=num_seconds, exception=TraceSearchTimeOut) + def generate_single_trace(self=self, plan_len=plan_len): + """Generates a single trace using the uniform random sampling technique. + Loops until a valid trace is found. The timer wrapper does not allow the function + to run past the time specified. + + The outside function is a wrapper that provides parameters for both the timer + wrapper and the function. + + Returns: + A Trace object (the valid trace generated). + """ + + if not plan_len: + plan_len = self.plan_len + + trace = Trace() + + state = self.problem.init + valid_trace = False + while not valid_trace: + trace.clear() + # add more steps while the trace has not yet reached the desired length + for j in range(plan_len): + # if we have not yet reached the last step + if len(trace) < plan_len - 1: + # find the next applicable actions + app_act = list(self.instance.applicable(state)) + # if the trace reaches a dead lock, disregard this trace and try again + if not app_act: + break + # pick a random applicable action and apply it + act = random.choice(app_act) + # create the trace and progress the state + macq_action = self.tarski_act_to_macq(act) + macq_state = self.tarski_state_to_macq(state) + step = Step(macq_state, macq_action, j + 1) + trace.append(step) + state = progress(state, act) + else: + macq_state = self.tarski_state_to_macq(state) + step = Step(state=macq_state, action=None, index=j + 1) + trace.append(step) + valid_trace = True + return trace + return generate_single_trace diff --git a/tests/generate/pddl/test_random_goal_sampling.py b/tests/generate/pddl/test_random_goal_sampling.py index baf81498..df7b415c 100644 --- a/tests/generate/pddl/test_random_goal_sampling.py +++ b/tests/generate/pddl/test_random_goal_sampling.py @@ -11,10 +11,11 @@ random_sampler = RandomGoalSampling( dom=dom, prob=prob, - num_traces=3, - steps_deep=10, + num_traces=20, + steps_deep=20, subset_size_perc=0.1, - enforced_hill_climbing_sampling=False + enforced_hill_climbing_sampling=False, + max_time=15 ) traces = random_sampler.traces traces.print(wrap="y") diff --git a/tests/generate/pddl/test_vanilla_sampling.py b/tests/generate/pddl/test_vanilla_sampling.py index 98538245..5cc3ece1 100644 --- a/tests/generate/pddl/test_vanilla_sampling.py +++ b/tests/generate/pddl/test_vanilla_sampling.py @@ -4,6 +4,7 @@ from macq.generate.pddl.generator import InvalidGoalFluent from macq.generate import InvalidNumberOfTraces, InvalidPlanLength from macq.trace import Fluent, PlanningObject, TraceList +from macq.utils import TraceSearchTimeOut def test_invalid_vanilla_sampling(): @@ -30,15 +31,21 @@ def test_invalid_vanilla_sampling(): "new_blocks_dom.pddl", "new_blocks_prob.pddl", ) + + with pytest.raises(TraceSearchTimeOut): + VanillaSampling(dom=dom, prob=prob, plan_len=10, num_traces=1, max_time=5) if __name__ == "__main__": # exit out to the base macq folder so we can get to /tests base = Path(__file__).parent.parent.parent + dom = str((base / "pddl_testing_files/blocks_domain.pddl").resolve()) prob = str((base / "pddl_testing_files/blocks_problem.pddl").resolve()) vanilla = VanillaSampling(dom=dom, prob=prob, plan_len=7, num_traces=10) - + traces = vanilla.traces + traces.generate_more(3) + new_blocks_dom = str((base / "generated_testing_files/new_blocks_dom.pddl").resolve()) new_blocks_prob = str((base / "generated_testing_files/new_blocks_prob.pddl").resolve()) new_game_dom = str((base / "generated_testing_files/new_game_dom.pddl").resolve()) From 519470cac12839c6526a0bad0e2247537cda8254 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Mon, 30 Aug 2021 15:10:28 -0400 Subject: [PATCH 166/181] update docs, add error for invalid time --- macq/generate/pddl/random_goal_sampling.py | 6 ++---- macq/generate/pddl/vanilla_sampling.py | 9 +++++++-- macq/utils/__init__.py | 4 ++-- macq/utils/timer.py | 17 ++++++++++++++--- .../generate/pddl/test_random_goal_sampling.py | 2 +- tests/generate/pddl/test_vanilla_sampling.py | 5 ++++- 6 files changed, 30 insertions(+), 13 deletions(-) diff --git a/macq/generate/pddl/random_goal_sampling.py b/macq/generate/pddl/random_goal_sampling.py index 27a575b2..aad3dc9f 100644 --- a/macq/generate/pddl/random_goal_sampling.py +++ b/macq/generate/pddl/random_goal_sampling.py @@ -8,10 +8,6 @@ from ...utils.timer import basic_timer - -MAX_GOAL_SEARCH_TIME = 30.0 - - class RandomGoalSampling(VanillaSampling): """Random Goal State Trace Sampler - inherits the VanillaSampling class and its attributes. @@ -64,6 +60,8 @@ def __init__( The problem filename. problem_id (int): The ID of the problem to access. + max_time (float): + The maximum time allowed for a trace to be generated. """ if subset_size_perc < 0 or subset_size_perc > 1: raise PercentError() diff --git a/macq/generate/pddl/vanilla_sampling.py b/macq/generate/pddl/vanilla_sampling.py index 1cef7f0d..177a95e7 100644 --- a/macq/generate/pddl/vanilla_sampling.py +++ b/macq/generate/pddl/vanilla_sampling.py @@ -1,8 +1,7 @@ from tarski.search.operations import progress import random from . import Generator -from ...utils import set_timer_throw_exc, TraceSearchTimeOut, basic_timer, set_num_traces, set_plan_length -from ...observation.partial_observation import PercentError +from ...utils import set_timer_throw_exc, TraceSearchTimeOut, set_num_traces, set_plan_length, InvalidTime from ...trace import ( Step, Trace, @@ -17,6 +16,8 @@ class VanillaSampling(Generator): of the given length. Attributes: + max_time (float): + The maximum time allowed for a trace to be generated. plan_len (int): The length of the traces to be generated. num_traces (int): @@ -50,8 +51,12 @@ def __init__( The problem filename. problem_id (int): The ID of the problem to access. + max_time (float): + The maximum time allowed for a trace to be generated. """ super().__init__(dom=dom, prob=prob, problem_id=problem_id) + if max_time <= 0: + raise InvalidTime() self.max_time = max_time self.plan_len = set_plan_length(plan_len) self.num_traces = set_num_traces(num_traces) diff --git a/macq/utils/__init__.py b/macq/utils/__init__.py index 98f4f2f4..f4aef5c8 100644 --- a/macq/utils/__init__.py +++ b/macq/utils/__init__.py @@ -1,6 +1,6 @@ -from .timer import set_timer_throw_exc, basic_timer, TraceSearchTimeOut +from .timer import set_timer_throw_exc, basic_timer, TraceSearchTimeOut, InvalidTime from .complex_encoder import ComplexEncoder from .common_errors import PercentError from .trace_utils import set_num_traces, set_plan_length -__all__ = ["set_timer_throw_exc", "basic_timer", "TraceSearchTimeOut", "ComplexEncoder", "PercentError"] +__all__ = ["set_timer_throw_exc", "basic_timer", "TraceSearchTimeOut", "InvalidTime", "ComplexEncoder", "PercentError", "set_num_traces", "set_plan_length"] diff --git a/macq/utils/timer.py b/macq/utils/timer.py index 561b7b54..4b9aeb8c 100644 --- a/macq/utils/timer.py +++ b/macq/utils/timer.py @@ -60,12 +60,23 @@ def wrapper(*args, **kwargs): class TraceSearchTimeOut(Exception): """ Raised when the time it takes to generate (or attempt to generate) a single trace is - longer than the MAX_TRACE_TIME constant. MAX_TRACE_TIME is 30 seconds by default. + longer than the generator's `max_time` attribute. """ def __init__( self, - message="The generator took longer than MAX_TRACE_TIME in its attempt to generate a trace. " - + "MAX_TRACE_TIME can be changed through the trace generator used.", + message="The generator took longer than `max_time` in its attempt to generate a trace. " + + "Change the `max_time` attribute for the trace generator used if you would like to have more time to generate a trace.", ): super().__init__(message) + +class InvalidTime(Exception): + """ + Raised when the user supplies an invalid maximum time for a trace to be generated + to a generator. + """ + def __init__( + self, + message="The provided maximum time is invalid.", + ): + super().__init__(message) \ No newline at end of file diff --git a/tests/generate/pddl/test_random_goal_sampling.py b/tests/generate/pddl/test_random_goal_sampling.py index df7b415c..581e4f5a 100644 --- a/tests/generate/pddl/test_random_goal_sampling.py +++ b/tests/generate/pddl/test_random_goal_sampling.py @@ -15,7 +15,7 @@ steps_deep=20, subset_size_perc=0.1, enforced_hill_climbing_sampling=False, - max_time=15 + max_time=-15 ) traces = random_sampler.traces traces.print(wrap="y") diff --git a/tests/generate/pddl/test_vanilla_sampling.py b/tests/generate/pddl/test_vanilla_sampling.py index 5cc3ece1..e9959754 100644 --- a/tests/generate/pddl/test_vanilla_sampling.py +++ b/tests/generate/pddl/test_vanilla_sampling.py @@ -4,7 +4,7 @@ from macq.generate.pddl.generator import InvalidGoalFluent from macq.generate import InvalidNumberOfTraces, InvalidPlanLength from macq.trace import Fluent, PlanningObject, TraceList -from macq.utils import TraceSearchTimeOut +from macq.utils import TraceSearchTimeOut, InvalidTime def test_invalid_vanilla_sampling(): @@ -34,6 +34,9 @@ def test_invalid_vanilla_sampling(): with pytest.raises(TraceSearchTimeOut): VanillaSampling(dom=dom, prob=prob, plan_len=10, num_traces=1, max_time=5) + + with pytest.raises(InvalidTime): + VanillaSampling(dom=dom, prob=prob, plan_len=10, num_traces=1, max_time=0) if __name__ == "__main__": From 20c9ab1eccfadec88acd2ac4148181523d83b312 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 30 Aug 2021 15:48:41 -0400 Subject: [PATCH 167/181] Add doc generation to CONTRIBUTING.md --- CONTRIBUTING.md | 13 +++++++++++-- macq/extract/arms.py | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4025d8a6..b13c3d30 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,12 +5,12 @@ ### Installing Install macq for development by cloning the repository and running -`pip install .[dev]` +`pip install -e .[dev]` We recommend installing in a virtual environment to avoid package version conflicts. -**`tarski` requires [`clingo`](https://potassco.org/clingo/).** +**Note: `tarski` requires [`clingo`](https://potassco.org/clingo/) be installed to work.** ### Formatting @@ -36,3 +36,12 @@ report, run `pytest --cov=macq --cov-report=html`, and open `htmlcov/index.html` in a browser. This will provide detailed line by line test coverage information, so you can identify what specifically still needs testing. +### Generating Docs +To generate the HTML documentation, run `pdoc --html macq --config latex_math=True`. + +During development, you can run a local HTTP server to reference/see live +changes to the documentation: `pdoc --http : macq --config latex_math=True`. + +*Note: `--config latex_math=True` is required to properly render the latex found +in many extraction techniques' documentation.* + diff --git a/macq/extract/arms.py b/macq/extract/arms.py index 08d27987..615ec377 100644 --- a/macq/extract/arms.py +++ b/macq/extract/arms.py @@ -368,8 +368,8 @@ def step2A( A1. The intersection of the precondition and add lists of all actions must be empty. A2. In addition, if an action’s delete list includes a relation, this relation is - in the action’s precondition list. Thus, for every action, we require that - the delete list is a subset of the precondition list. + in the action’s precondition list. Thus, for every action, we require that + the delete list is a subset of the precondition list. """ if debug: From 34fe41bf6ddd3349efd0503724d3125b4527355f Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 30 Aug 2021 15:49:29 -0400 Subject: [PATCH 168/181] Add ... to SLAF docs --- docs/extract/slaf.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/extract/slaf.md b/docs/extract/slaf.md index f333eefe..49acbf64 100644 --- a/docs/extract/slaf.md +++ b/docs/extract/slaf.md @@ -13,7 +13,7 @@ print(model.details()) **Output:** ```text Model: - Fluents: clear location pos-06-09, clear location pos-02-05, clear location pos-08-08, clear location pos-10-05, clear location pos-02-06, clear location pos-10-02, clear location pos-01-01, at stone stone-05 location pos-08-05, at stone stone-07 location pos-08-06, at stone stone-03 location pos-07-04, clear location pos-03-06, clear location pos-10-06, clear location pos-10-10, clear location pos-05-09, clear location pos-05-07, clear location pos-02-07, clear location pos-09-01, at stone stone-06 location pos-04-06, clear location pos-02-03, clear location pos-07-05, clear location pos-09-10, clear location pos-06-05, at stone stone-01 location pos-05-04, clear location pos-02-10, clear location pos-06-10, clear location pos-11-03, at stone stone-11 location pos-06-08, at stone stone-08 location pos-04-07, clear location pos-01-10, clear location pos-07-03, clear location pos-02-11, clear location pos-03-01, clear location pos-06-02, clear location pos-03-02, clear location pos-11-01, clear location pos-06-03, clear location pos-08-04, clear location pos-09-11, at stone stone-09 location pos-08-07, clear location pos-09-07, clear location pos-06-07, clear location pos-10-01, clear location pos-11-09, clear location pos-03-05, clear location pos-07-06, clear location pos-05-05, at stone stone-12 location pos-07-08, clear location pos-10-03, clear location pos-11-11, clear location pos-10-09, clear location pos-02-01, clear location pos-02-02, clear location pos-01-02, at stone stone-02 location pos-06-04, clear location pos-03-10, clear location pos-05-10, clear location pos-07-10, clear location pos-09-05, clear location pos-07-09, clear location pos-05-03, clear location pos-10-11, clear location pos-01-03, at stone stone-04 location pos-04-05, clear location pos-07-02, clear location pos-09-06, clear location pos-10-07, clear location pos-01-09, clear location pos-03-07, clear location pos-04-04, clear location pos-01-11 + Fluents: clear location pos-06-09, clear location pos-02-05, clear location pos-08-08, clear location pos-10-05, ... Actions: move player player-01 direction dir-left location pos-05-02 location pos-06-02: precond: @@ -21,6 +21,8 @@ Model: delete: (clear location pos-05-02) (at player player-01 location pos-06-02) + ... + ... ``` # API Documentation From 5ed6e3067153144d5f801a47f83fd8130b0ffd3c Mon Sep 17 00:00:00 2001 From: beckydvn Date: Mon, 30 Aug 2021 16:18:54 -0400 Subject: [PATCH 169/181] fix init issue from merge --- macq/extract/__init__.py | 3 ++- macq/extract/amdn.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/macq/extract/__init__.py b/macq/extract/__init__.py index 70b87c86..8a236f86 100644 --- a/macq/extract/__init__.py +++ b/macq/extract/__init__.py @@ -1,5 +1,6 @@ from .learned_fluent import LearnedFluent from .learned_action import LearnedAction +from .model import Model, LearnedAction from .extract import Extract, modes from .exceptions import IncompatibleObservationToken from .model import Model @@ -11,4 +12,4 @@ "Extract", "modes", "IncompatibleObservationToken", -] +] \ No newline at end of file diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 70730dda..560b17cc 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -11,8 +11,8 @@ InvalidMaxSATModel, ) from .model import Model -from ..trace import ObservationLists, ActionPair -from ..observation import NoisyPartialDisorderedParallelObservation +from ..trace import ActionPair +from ..observation import NoisyPartialDisorderedParallelObservation, ObservationLists from ..utils.pysat import to_wcnf e = Encoding From 0bb753ce20cb5290d570cc95288c25b2e31fecb9 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Mon, 30 Aug 2021 16:47:42 -0400 Subject: [PATCH 170/181] show current time attribute when error is raised --- macq/generate/pddl/vanilla_sampling.py | 2 +- macq/utils/timer.py | 8 ++++---- tests/generate/pddl/test_vanilla_sampling.py | 6 +++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/macq/generate/pddl/vanilla_sampling.py b/macq/generate/pddl/vanilla_sampling.py index fff9869c..ab20b5ac 100644 --- a/macq/generate/pddl/vanilla_sampling.py +++ b/macq/generate/pddl/vanilla_sampling.py @@ -81,7 +81,7 @@ def generate_traces(self): return traces def generate_single_trace_setup(self, num_seconds: float, plan_len: int = None): - @set_timer_throw_exc(num_seconds=num_seconds, exception=TraceSearchTimeOut) + @set_timer_throw_exc(num_seconds=num_seconds, exception=TraceSearchTimeOut, max_time=num_seconds) def generate_single_trace(self=self, plan_len=plan_len): """Generates a single trace using the uniform random sampling technique. Loops until a valid trace is found. The timer wrapper does not allow the function diff --git a/macq/utils/timer.py b/macq/utils/timer.py index 4b9aeb8c..9bad9298 100644 --- a/macq/utils/timer.py +++ b/macq/utils/timer.py @@ -2,7 +2,7 @@ from typing import Union -def set_timer_throw_exc(num_seconds: Union[float, int], exception: Exception): +def set_timer_throw_exc(num_seconds: Union[float, int], exception: Exception, *exception_args, **exception_kwargs): def timer(function): """ Checks that a function runs within the specified time and raises an exception if it doesn't. @@ -27,7 +27,7 @@ def wrapper(*args, **kwargs): return thr.get() else: # otherwise, raise an exception if the function takes too long - raise exception() + raise exception(*exception_args, **exception_kwargs) return wrapper @@ -65,9 +65,9 @@ class TraceSearchTimeOut(Exception): def __init__( self, - message="The generator took longer than `max_time` in its attempt to generate a trace. " - + "Change the `max_time` attribute for the trace generator used if you would like to have more time to generate a trace.", + max_time: float ): + message=f"The generator could not find a suitable trace in {max_time} seconds or less. Change the `max_time` attribute for the trace generator used if you would like to have more time to generate a trace." super().__init__(message) class InvalidTime(Exception): diff --git a/tests/generate/pddl/test_vanilla_sampling.py b/tests/generate/pddl/test_vanilla_sampling.py index 3740e9f8..23eff7ff 100644 --- a/tests/generate/pddl/test_vanilla_sampling.py +++ b/tests/generate/pddl/test_vanilla_sampling.py @@ -48,6 +48,10 @@ def test_invalid_vanilla_sampling(): vanilla = VanillaSampling(dom=dom, prob=prob, plan_len=7, num_traces=10) traces = vanilla.traces traces.generate_more(3) + + dom = str((base / "pddl_testing_files/playlist_domain.pddl").resolve()) + prob = str((base / "pddl_testing_files/playlist_problem.pddl").resolve()) + VanillaSampling(dom=dom, prob=prob, plan_len=10, num_traces=10, max_time=3) new_blocks_dom = str((base / "generated_testing_files/new_blocks_dom.pddl").resolve()) new_blocks_prob = str((base / "generated_testing_files/new_blocks_prob.pddl").resolve()) @@ -97,5 +101,5 @@ def test_invalid_vanilla_sampling(): # test generating traces with action preconditions/effects known vanilla_traces = VanillaSampling( - problem_id=4627, plan_len=7, num_traces=10, observe_pres_effs=True + problem_id=123, plan_len=7, num_traces=10, observe_pres_effs=True ).traces From d640101f826fe84cb61349969cec9b8c664b802b Mon Sep 17 00:00:00 2001 From: beckydvn Date: Mon, 30 Aug 2021 17:07:40 -0400 Subject: [PATCH 171/181] update to match observationlist changes --- macq/trace/disordered_parallel_actions_observation_lists.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index 81233632..e40d555d 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -101,7 +101,7 @@ class DisorderedParallelActionsObservationLists(ObservationLists): The default feature functions and theta vector described in the AMDN paper are available for use in this module. Attributes: - traces (List[List[Token]]): + observations (List[List[Token]]): The trace list converted to a list of lists of tokens. type (Type[Observation]): The type of token to be used. @@ -150,7 +150,7 @@ def __init__( **kwargs: Any extra arguments to be supplied to the Token __init__. """ - self.traces = [] + self.observations = [] self.type = Token self.all_par_act_sets = [] self.all_states = [] From e82dc44423b192561d095c06e5ec3b691779ed45 Mon Sep 17 00:00:00 2001 From: ecal Date: Mon, 30 Aug 2021 17:47:20 -0400 Subject: [PATCH 172/181] Add progress module to utils, select function based on tqdm avail. --- macq/utils/progress.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 macq/utils/progress.py diff --git a/macq/utils/progress.py b/macq/utils/progress.py new file mode 100644 index 00000000..709c8a31 --- /dev/null +++ b/macq/utils/progress.py @@ -0,0 +1,18 @@ +try: + from tqdm import tqdm, trange + + TQDM = True + +except ModuleNotFoundError: + TQDM = False + + +def tqdm_progress(iterable=None): + pass + + +def vanilla_progress(iterable): + pass + + +progress = tqdm_progress if TQDM else vanilla_progress From e9d71ccbcb20c144aa8d50575d2e2cc6780481df Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 31 Aug 2021 10:32:57 -0400 Subject: [PATCH 173/181] Add tqdm redirect, and fallback progress counter --- macq/utils/__init__.py | 2 ++ macq/utils/progress.py | 39 +++++++++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/macq/utils/__init__.py b/macq/utils/__init__.py index 1c45496f..9764751d 100644 --- a/macq/utils/__init__.py +++ b/macq/utils/__init__.py @@ -4,6 +4,7 @@ from .trace_errors import InvalidPlanLength, InvalidNumberOfTraces from .trace_utils import set_num_traces, set_plan_length from .tokenization_errors import TokenizationError +from .progress import progress # from .tokenization_utils import extract_fluent_subset @@ -18,4 +19,5 @@ "InvalidPlanLength", "InvalidNumberOfTraces", "TokenizationError", + "progress", ] diff --git a/macq/utils/progress.py b/macq/utils/progress.py index 709c8a31..853444ae 100644 --- a/macq/utils/progress.py +++ b/macq/utils/progress.py @@ -7,12 +7,39 @@ TQDM = False -def tqdm_progress(iterable=None): - pass - - -def vanilla_progress(iterable): - pass +def tqdm_progress(iterable=None, *args, **kwargs): + if isinstance(iterable, range): + return trange(iterable.start, iterable.stop, iterable.step, *args, **kwargs) + return tqdm(iterable, *args, **kwargs) + + +class vanilla_progress: + def __init__(self, iterable, *args, **kwargs): + self.iterable = iterable + self.args = args + self.kwargs = kwargs + + def __iter__(self): + if isinstance(self.iterable, range): + start = self.iterable.start + stop = self.iterable.stop + step = self.iterable.step + total = (stop - start) / step + else: + total = len(self.iterable) + + prev = 0 + it = 1 + for i in self.iterable: + yield i + new = int(str(it / total)[2]) + if new != prev: + prev = new + if new == 0: + print("100%") + else: + print(f"{new}0% ...") + it += 1 progress = tqdm_progress if TQDM else vanilla_progress From 57530d86c9d6ac01dff1e43473d6db1bd6931c33 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 31 Aug 2021 11:54:44 -0400 Subject: [PATCH 174/181] use union of action effs for states in between --- ...ered_parallel_actions_observation_lists.py | 30 +++++++++++++++---- tests/extract/test_amdn.py | 4 +-- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index e40d555d..7d09930a 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -3,7 +3,7 @@ from numpy import dot from random import random from typing import Callable, Type, Set, List -from . import TraceList, Step, Action +from . import TraceList, Step, Action, PartialState, State from ..observation import Observation, ObservationLists @@ -156,7 +156,6 @@ def __init__( self.all_states = [] self.features = features self.learned_theta = learned_theta - # TODO: use functions here instead? actions = {step.action for trace in traces for step in trace if step.action} # cast to list for iteration purposes self.actions = list(actions) @@ -237,6 +236,25 @@ def _calculate_all_probabilities(self): probabilities[combo] = self._calculate_probability(*combo.tup()) return probabilities + def _get_new_partial_state(self): + """ + Return a PartialState with the fluents used in this observation, with each fluent set to None as default. + """ + cur_state = PartialState() + for f in self.propositions: + cur_state[f] = None + return cur_state + + def _update_partial_state(self, partial_state: PartialState, orig_state: State, action: Action): + """ + Update the provided PartialState with the fluents provided. + """ + new_partial = partial_state.copy() + effects = set([e for e in action.add] + [e for e in action.delete]) + for e in effects: + new_partial[e] = orig_state[e] + return new_partial + def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): """Main driver that handles the tokenization process. @@ -256,8 +274,8 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): cur_par_act_conditions = set() # add initial state states.append(trace[0].state) - # for the compiler - cur_state = trace[1].state + + cur_state = self._get_new_partial_state() # last step doesn't have an action/just contains the state after the last action for i in range(len(trace)): @@ -273,6 +291,8 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): # add psi_k and s'_k to the final (ordered) lists of parallel action sets and states par_act_sets.append(cur_par_act) states.append(cur_state) + # reset the state + cur_state = self._get_new_partial_state() # reset psi_k (that is, create a new parallel action set) cur_par_act = set() # reset the conditions @@ -280,7 +300,7 @@ def tokenize(self, traces: TraceList, Token: Type[Observation], **kwargs): # add the action and state to the appropriate psi_k and s'_k (either the existing ones, or # new/empty ones if the current action is NOT parallel with actions in the previous set of actions.) cur_par_act.add(a) - cur_state = trace[i + 1].state + cur_state = self._update_partial_state(cur_state, trace[i + 1].state, trace[i].action) cur_par_act_conditions.update(a_conditions) # if on the last step of the trace, add the current set/state to the final result before exiting the loop if i == len(trace) - 1: diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 3ddc672b..1b031ccf 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -180,12 +180,12 @@ def test_tracelist(): observe_pres_effs=True, num_traces=1, # plan_len=10, - steps_deep=30, + steps_deep=4, subset_size_perc=0.1, enforced_hill_climbing_sampling=True, ).traces - # traces = test_tracelist() + traces = test_tracelist() # dom = str((base / "pddl_testing_files/door_dom.pddl").resolve()) # prob = str((base / "pddl_testing_files/door_prob.pddl").resolve()) From 89cb0116adbabfaf047590f9f40e8ff69e236b98 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 31 Aug 2021 14:35:46 -0400 Subject: [PATCH 175/181] clean up --- tests/extract/test_amdn.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tests/extract/test_amdn.py b/tests/extract/test_amdn.py index 1b031ccf..9717d5e9 100644 --- a/tests/extract/test_amdn.py +++ b/tests/extract/test_amdn.py @@ -167,26 +167,25 @@ def test_tracelist(): if __name__ == "__main__": # exit out to the base macq folder so we can get to /tests base = Path(__file__).parent.parent - + + # use blocksworld (NOTE: no actions are parallel in this domain) dom = str((base / "pddl_testing_files/blocks_domain.pddl").resolve()) prob = str((base / "pddl_testing_files/blocks_problem.pddl").resolve()) - # TODO: replace with a domain-specific random trace generator traces = RandomGoalSampling( - # traces = VanillaSampling( prob=prob, dom=dom, - # problem_id=2337, observe_pres_effs=True, num_traces=1, - # plan_len=10, steps_deep=4, subset_size_perc=0.1, enforced_hill_climbing_sampling=True, ).traces - traces = test_tracelist() + # use the simple truck domain for debugging + # traces = test_tracelist() + # use the simple door domain for debugging # dom = str((base / "pddl_testing_files/door_dom.pddl").resolve()) # prob = str((base / "pddl_testing_files/door_prob.pddl").resolve()) # traces = TraceList([TraceFromGoal(dom=dom, prob=prob, observe_pres_effs=True).trace]) From 2fa042d641befc6c5687160f32c9e3d1aee4dbb9 Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 31 Aug 2021 14:45:51 -0400 Subject: [PATCH 176/181] cleanup, move common function in amdn out to pysat --- macq/extract/amdn.py | 60 +------------------ ...ered_parallel_actions_observation_lists.py | 3 + macq/utils/pysat.py | 31 ++++++++++ 3 files changed, 37 insertions(+), 57 deletions(-) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index 560b17cc..e21473bc 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -1,19 +1,16 @@ from macq.extract.learned_action import LearnedAction from nnf.operators import implies import macq.extract as extract -from typing import Dict, List, Optional, Union, Set, Hashable +from typing import Dict, List, Optional, Union, Hashable from nnf import Aux, Var, And, Or -from pysat.formula import WCNF -from pysat.examples.rc2 import RC2 from bauhaus import Encoding # only used for pretty printing in debug mode from .exceptions import ( IncompatibleObservationToken, - InvalidMaxSATModel, ) from .model import Model from ..trace import ActionPair from ..observation import NoisyPartialDisorderedParallelObservation, ObservationLists -from ..utils.pysat import to_wcnf +from ..utils.pysat import to_wcnf, extract_raw_model e = Encoding @@ -57,7 +54,7 @@ def __new__(cls, obs_lists: ObservationLists, debug: bool = False, occ_threshold @staticmethod def _amdn(obs_lists: ObservationLists, debug: int, occ_threshold: int): wcnf, decode = AMDN._solve_constraints(obs_lists, occ_threshold, debug) - raw_model = AMDN._extract_raw_model(wcnf, decode) + raw_model = extract_raw_model(wcnf, decode) return AMDN._extract_model(obs_lists, raw_model) @staticmethod @@ -387,7 +384,6 @@ def _set_all_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: parallel_constraints = AMDN._build_parallel_constraints(obs_lists, debug, to_obs) noise_constraints = AMDN._build_noise_constraints(obs_lists, occ_threshold, debug, to_obs) return {**disorder_constraints, **parallel_constraints, **noise_constraints} - #return {**parallel_constraints, **noise_constraints} @staticmethod def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int): @@ -398,62 +394,12 @@ def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: i for c, weight in constraints.items(): if weight == "HARD": hard_constraints.append(c) - for c in hard_constraints: del constraints[c] - # NOTE: just a test - # r = list(obs_lists.propositions)[0] - # act = list(obs_lists.actions)[0] - - # hard parallel constraints 1: successfully fails the model, but ONLY if WMAX is not used and they are set as direct hard constraints - # hard_constraints.append(AMDN._or_refactor(add(r, act))) - # hard_constraints.append(AMDN._or_refactor(pre(r, act))) - # hard parallel constraints 2: successfully fails the model, but ONLY if WMAX is not used and they are set as direct hard constraints - #hard_constraints.append(AMDN._or_refactor(delete(r, act))) - #hard_constraints.append(AMDN._or_refactor(~pre(r, act))) - - #act_x = list(list(obs_lists.all_par_act_sets[0])[0])[0] - #act_y = list(list(obs_lists.all_par_act_sets[0])[1])[0] - - # attempt to force the model - # hard_constraints.append(AMDN._or_refactor(Var("(rooma is a precondition of open )"))) - # hard_constraints.append(AMDN._or_refactor(Var("(open is added by open )"))) - # hard_constraints.append(AMDN._or_refactor(Var("(rooma is a precondition of walk )"))) - # hard_constraints.append(AMDN._or_refactor(Var("(open is a precondition of walk )"))) - # hard_constraints.append(AMDN._or_refactor(Var("(rooma is deleted by walk )"))) - # hard_constraints.append(AMDN._or_refactor(Var("(roomb is added by walk )"))) - - # hard_constraints.append(AMDN._or_refactor(Var("(truck red_truck is a precondition of drive red_truck location_a location_b)"))) - # hard_constraints.append(AMDN._or_refactor(Var("(place location_a is a precondition of drive red_truck location_a location_b)"))) - # hard_constraints.append(AMDN._or_refactor(Var("(place location_b is a precondition of drive red_truck location_a location_b)"))) - # hard_constraints.append(AMDN._or_refactor(Var("(at red_truck location_a is a precondition of drive red_truck location_a location_b)"))) - # TODO: add add/del effects if you're going to use these - # hard_constraints.append(AMDN._or_refactor(Var("(truck blue_truck is a precondition of drive blue_truck location_c location_d)"))) - # hard_constraints.append(AMDN._or_refactor(Var("(place location_c is a precondition of drive blue_truck location_c location_d)"))) - # hard_constraints.append(AMDN._or_refactor(Var("(place location_d is a precondition of drive blue_truck location_c location_d)"))) - # hard_constraints.append(AMDN._or_refactor(Var("(at blue_truck location_c is a precondition of drive blue_truck location_c location_d)"))) - wcnf, decode = to_wcnf(soft_clauses=And(constraints.keys()), hard_clauses=And(hard_constraints), weights=list(constraints.values())) - return wcnf, decode - # TODO: move out to utils - @staticmethod - def _extract_raw_model(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: - solver = RC2(max_sat) - encoded_model = solver.compute() - - if not isinstance(encoded_model, list): - # should never be reached - raise InvalidMaxSATModel(encoded_model) - - # decode the model (back to nnf vars) - model: Dict[Hashable, bool] = { - decode[abs(clause)]: clause > 0 for clause in encoded_model - } - return model - @staticmethod def _split_raw_fluent(raw_f: Hashable, learned_actions: Dict[str, LearnedAction]): raw_f = str(raw_f)[1:-1] diff --git a/macq/trace/disordered_parallel_actions_observation_lists.py b/macq/trace/disordered_parallel_actions_observation_lists.py index 7d09930a..451f5d5e 100644 --- a/macq/trace/disordered_parallel_actions_observation_lists.py +++ b/macq/trace/disordered_parallel_actions_observation_lists.py @@ -186,6 +186,9 @@ def _theta_dot_features_calc(f_vec: List[float], theta_vec: List[float]): return exp(dot(f_vec, theta_vec)) def _calculate_denom(self): + """ + Calculates and returns the denominator used in probability calculations. + """ denominator = 0 for combo in self.cross_actions: denominator += self._theta_dot_features_calc(self._get_f_vec(*combo.tup()), self.learned_theta) diff --git a/macq/utils/pysat.py b/macq/utils/pysat.py index 5a9cf2a5..464d0f40 100644 --- a/macq/utils/pysat.py +++ b/macq/utils/pysat.py @@ -2,6 +2,7 @@ from pysat.formula import WCNF from pysat.examples.rc2 import RC2 from nnf import And, Or, Var +from ..extract.exceptions import InvalidMaxSATModel def get_encoding( @@ -73,3 +74,33 @@ def to_wcnf( wcnf.extend(encoded) return wcnf, decode + +def extract_raw_model(max_sat: WCNF, decode: Dict[int, Hashable]) -> Dict[Hashable, bool]: + """Extracts a raw model given a WCNF and the corresponding decoding dictionary. + + Args: + max_sat (WCNF): + The WCNF to solve for. + decode (Dict[int, Hashable]): + The decode dictionary mapping to convert the pysat vars back to NNF. + + Raises: + InvalidMaxSATModel: + If the model is invalid. + + Returns: + Dict[Hashable, bool]: + The raw model. + """ + solver = RC2(max_sat) + encoded_model = solver.compute() + + if not isinstance(encoded_model, list): + # should never be reached + raise InvalidMaxSATModel(encoded_model) + + # decode the model (back to nnf vars) + model: Dict[Hashable, bool] = { + decode[abs(clause)]: clause > 0 for clause in encoded_model + } + return model From 7929ce922e41781b4f709b27831ab5f6a1a37593 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 31 Aug 2021 15:03:20 -0400 Subject: [PATCH 177/181] Handle Sized and Unsized iterables --- macq/utils/progress.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/macq/utils/progress.py b/macq/utils/progress.py index 853444ae..82d16865 100644 --- a/macq/utils/progress.py +++ b/macq/utils/progress.py @@ -1,3 +1,6 @@ +from typing import Iterable, Sized + + try: from tqdm import tqdm, trange @@ -14,6 +17,8 @@ def tqdm_progress(iterable=None, *args, **kwargs): class vanilla_progress: + iterable: Iterable + def __init__(self, iterable, *args, **kwargs): self.iterable = iterable self.args = args @@ -25,20 +30,23 @@ def __iter__(self): stop = self.iterable.stop step = self.iterable.step total = (stop - start) / step - else: + elif isinstance(self.iterable, Sized): total = len(self.iterable) + else: + total = None prev = 0 it = 1 for i in self.iterable: yield i - new = int(str(it / total)[2]) - if new != prev: - prev = new - if new == 0: - print("100%") - else: - print(f"{new}0% ...") + if total is not None: + new = int(str(it / total)[2]) + if new != prev: + prev = new + if new == 0: + print("100%") + else: + print(f"{new}0% ...") it += 1 From dfa18d5020f67a32fb523bd7ded19e410ffeda4f Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 31 Aug 2021 15:05:28 -0400 Subject: [PATCH 178/181] Use Any as return type --- macq/utils/progress.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/macq/utils/progress.py b/macq/utils/progress.py index 82d16865..d5b5d9a7 100644 --- a/macq/utils/progress.py +++ b/macq/utils/progress.py @@ -1,4 +1,4 @@ -from typing import Iterable, Sized +from typing import Iterable, Iterator, Sized, Any try: @@ -10,7 +10,7 @@ TQDM = False -def tqdm_progress(iterable=None, *args, **kwargs): +def tqdm_progress(iterable=None, *args, **kwargs) -> Any: if isinstance(iterable, range): return trange(iterable.start, iterable.stop, iterable.step, *args, **kwargs) return tqdm(iterable, *args, **kwargs) @@ -24,7 +24,7 @@ def __init__(self, iterable, *args, **kwargs): self.args = args self.kwargs = kwargs - def __iter__(self): + def __iter__(self) -> Iterator[Any]: if isinstance(self.iterable, range): start = self.iterable.start stop = self.iterable.stop From 5bfecf72f34878b82d94b07030d636f5a76c80f8 Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 31 Aug 2021 15:10:43 -0400 Subject: [PATCH 179/181] Wrap trace generation with progress bar --- macq/generate/pddl/random_goal_sampling.py | 5 ++--- macq/generate/pddl/vanilla_sampling.py | 17 ++++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/macq/generate/pddl/random_goal_sampling.py b/macq/generate/pddl/random_goal_sampling.py index c1c9f210..596bc62b 100644 --- a/macq/generate/pddl/random_goal_sampling.py +++ b/macq/generate/pddl/random_goal_sampling.py @@ -4,8 +4,7 @@ from collections import OrderedDict from . import VanillaSampling from ...trace import TraceList, State -from ...utils import PercentError -from ...utils.timer import basic_timer +from ...utils import PercentError, basic_timer, progress MAX_GOAL_SEARCH_TIME = 30.0 @@ -174,7 +173,7 @@ def generate_traces(self): # retrieve goals and their respective plans self.goals_inits_plans = self.goal_sampling() # iterate through all plans corresponding to the goals, generating traces - for goal in self.goals_inits_plans.values(): + for goal in progress(self.goals_inits_plans.values()): # update the initial state if necessary if self.enforced_hill_climbing_sampling: self.problem.init = goal["initial state"] diff --git a/macq/generate/pddl/vanilla_sampling.py b/macq/generate/pddl/vanilla_sampling.py index 2fb550a5..9485a948 100644 --- a/macq/generate/pddl/vanilla_sampling.py +++ b/macq/generate/pddl/vanilla_sampling.py @@ -4,14 +4,12 @@ from ...utils import ( set_timer_throw_exc, TraceSearchTimeOut, - basic_timer, set_num_traces, set_plan_length, + progress as print_progress, ) -from ...observation.partial_observation import PercentError from ...trace import ( Step, - State, Trace, TraceList, ) @@ -36,7 +34,7 @@ class VanillaSampling(Generator): """ def __init__( - self, + self, dom: str = None, prob: str = None, problem_id: int = None, @@ -57,14 +55,19 @@ def __init__( problem_id (int): The ID of the problem to access. observe_pres_effs (bool): - Option to observe action preconditions and effects upon generation. + Option to observe action preconditions and effects upon generation. plan_len (int): The length of each generated trace. Defaults to 1. num_traces (int): The number of traces to generate. Defaults to 1. """ - super().__init__(dom=dom, prob=prob, problem_id=problem_id, observe_pres_effs=observe_pres_effs) + super().__init__( + dom=dom, + prob=prob, + problem_id=problem_id, + observe_pres_effs=observe_pres_effs, + ) self.plan_len = set_plan_length(plan_len) self.num_traces = set_num_traces(num_traces) self.traces = self.generate_traces() @@ -80,7 +83,7 @@ def generate_traces(self): """ traces = TraceList() traces.generator = self.generate_single_trace - for _ in range(self.num_traces): + for _ in print_progress(range(self.num_traces)): traces.append(self.generate_single_trace()) return traces From 553ffcccc130bdc0a9caa44bffffee758bf00ead Mon Sep 17 00:00:00 2001 From: ecal Date: Tue, 31 Aug 2021 15:22:36 -0400 Subject: [PATCH 180/181] Add documentation --- macq/utils/progress.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/macq/utils/progress.py b/macq/utils/progress.py index d5b5d9a7..341a303f 100644 --- a/macq/utils/progress.py +++ b/macq/utils/progress.py @@ -11,15 +11,22 @@ def tqdm_progress(iterable=None, *args, **kwargs) -> Any: + """Wraps a loop with tqdm to output a progress bar.""" if isinstance(iterable, range): return trange(iterable.start, iterable.stop, iterable.step, *args, **kwargs) return tqdm(iterable, *args, **kwargs) class vanilla_progress: - iterable: Iterable + """Wraps a loop to output progress reports.""" - def __init__(self, iterable, *args, **kwargs): + def __init__(self, iterable: Iterable[Any], *args, **kwargs): + """Initializes a vanilla_progress object with the given iterable. + + Args: + iterable (Iterable): + The iterable to loop over and track the progress of. + """ self.iterable = iterable self.args = args self.kwargs = kwargs From aaa27cb13fcf53d01c7b59996da8d27a494f1f9b Mon Sep 17 00:00:00 2001 From: beckydvn Date: Tue, 31 Aug 2021 18:15:24 -0400 Subject: [PATCH 181/181] update docs and readme, clean up --- README.md | 2 +- macq/extract/amdn.py | 274 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 261 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 33b212c1..f934cc5d 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ Model: - [ ] [Learning STRIPS Action Models with Classical Planning](https://arxiv.org/abs/1903.01153) (ICAPS'18) - [ ] [Learning Planning Operators from Episodic Traces](https://aaai.org/ocs/index.php/SSS/SSS18/paper/view/17594/15530) (AAAI-SS'18) - [ ] [Learning action models with minimal observability](https://www.sciencedirect.com/science/article/abs/pii/S0004370218304259) (AIJ'19) -- [ ] [Learning Action Models from Disordered and Noisy Plan Traces](https://arxiv.org/abs/1908.09800) (arXiv'19) +- [x] [Learning Action Models from Disordered and Noisy Plan Traces](https://arxiv.org/abs/1908.09800) (arXiv'19) - [ ] [Bridging the Gap: Providing Post-Hoc Symbolic Explanations for Sequential Decision-Making Problems with Black Box Simulators](https://arxiv.org/abs/2002.01080) (ICML-WS'20) - [ ] [STRIPS Action Discovery](https://arxiv.org/abs/2001.11457) (arXiv'20) - [ ] [Learning First-Order Symbolic Representations for Planning from the Structure of the State Space](https://arxiv.org/abs/1909.05546) (ECAI'20) diff --git a/macq/extract/amdn.py b/macq/extract/amdn.py index e21473bc..a288b36e 100644 --- a/macq/extract/amdn.py +++ b/macq/extract/amdn.py @@ -1,3 +1,4 @@ +from macq.trace import Fluent, Action # for typing from macq.extract.learned_action import LearnedAction from nnf.operators import implies import macq.extract as extract @@ -14,20 +15,45 @@ e = Encoding -def _set_precond(r, act): +def pre(r: Fluent, act: Action): + """Create a Var that enforces that the given fluent is a precondition of the given action. + + Args: + r (Fluent): + The precondition to be added. + act (Action): + The action that the precondition will be added to. + Returns: + The Var that enforces that the given fluent is a precondition of the given action. + """ return Var("(" + str(r)[1:-1] + " is a precondition of " + act.details() + ")") -def _set_add(r, act): +def add(r: Fluent, act: Action): + """Create a Var that enforces that the given fluent is an add effect of the given action. + + Args: + r (Fluent): + The add effect to be added. + act (Action): + The action that the add effect will be added to. + Returns: + The Var that enforces that the given fluent is an add effect of the given action. + """ return Var("(" + str(r)[1:-1] + " is added by " + act.details() + ")") -def _set_del(r, act): +def delete(r: Fluent, act: Action): + """Create a Var that enforces that the given fluent is a delete effect of the given action. + + Args: + r (Fluent): + The delete effect to be added. + act (Action): + The action that the delete effect will be added to. + Returns: + The Var that enforces that the given fluent is a delete effect of the given action. + """ return Var("(" + str(r)[1:-1] + " is deleted by " + act.details() + ")") -# for easier reference -pre = _set_precond -add = _set_add -delete = _set_del - WMAX = 1 class AMDN: @@ -52,7 +78,23 @@ def __new__(cls, obs_lists: ObservationLists, debug: bool = False, occ_threshold return AMDN._amdn(obs_lists, debug, occ_threshold) @staticmethod - def _amdn(obs_lists: ObservationLists, debug: int, occ_threshold: int): + def _amdn(obs_lists: ObservationLists, debug: bool, occ_threshold: int): + """Main driver for the entire AMDN algorithm. + The first line contains steps 1-4. + The second line contains step 5. + Finally, the final line corresponds to step 6 (return the model). + + Args: + obs_lists (ObservationLists): + The tokens to be fed into the algorithm. + debug (bool): + Optional debugging mode. + occ_threshold (int): + Threshold to be used for noise constraints. + + Returns: + The extracted `Model`. + """ wcnf, decode = AMDN._solve_constraints(obs_lists, occ_threshold, debug) raw_model = extract_raw_model(wcnf, decode) return AMDN._extract_model(obs_lists, raw_model) @@ -72,6 +114,18 @@ def _or_refactor(maybe_lit: Union[Or, Var]): @staticmethod def _extract_aux_set_weights(cnf_formula: And[Or[Var]], constraints: Dict, prob_disordered: float): + """Sets each clause in a CNF formula as a hard constraint, then sets any auxiliary variables to + the appropriate weight detailed in the "Constraint DC" section of the AMDN paper. + Used to help create disorder constraints. + + Args: + cnf_formula (And[Or[Var]]): + The CNF formula to extract the clauses and auxiliary variables from. + constraints (Dict): + The existing dictionary of disorder constraints. + prob_disordered (float): + The probability that the two actions relevant fot this constraint are disordered. + """ # find all the auxiliary variables for clause in cnf_formula.children: for var in clause.children: @@ -83,6 +137,15 @@ def _extract_aux_set_weights(cnf_formula: And[Or[Var]], constraints: Dict, prob_ @staticmethod def _get_observe(obs_lists: ObservationLists): + """Gets from the user which fluents they want to observe (for debug mode). + + Args: + obs_lists (ObservationLists): + The tokens that contain the fluents. + + Returns: + A list of of which fluents the user wants to observe. + """ print("Select a proposition to observe:") sorted_f = [str(f) for f in obs_lists.propositions] sorted_f.sort() @@ -104,6 +167,17 @@ def _get_observe(obs_lists: ObservationLists): @staticmethod def _debug_is_observed(constraint: Or, to_obs: List[str]): + """Determines if the given constraint contains a fluent that is being observed in debug mode. + + Args: + constraint (Or): + The constraint to be analyzed. + to_obs (List[str]): + The list of fluents being observed. + + Returns: + A bool that determines if the constraint should be observed or not. + """ for c in constraint.children: for v in to_obs: if v in str(c): @@ -112,6 +186,14 @@ def _debug_is_observed(constraint: Or, to_obs: List[str]): @staticmethod def _debug_simple_pprint(constraints: Dict, to_obs: List[str]): + """Pretty print used for simple formulas in debug mode. + + Args: + constraints (Dict): + The constraints/weights to be pretty printed. + to_obs (List[str]): + The fluents being observed. + """ for c in constraints: observe = AMDN._debug_is_observed(c, to_obs) if observe: @@ -120,6 +202,14 @@ def _debug_simple_pprint(constraints: Dict, to_obs: List[str]): @staticmethod def _debug_aux_pprint(constraints: Dict, to_obs: List[str]): + """Pretty print used for formulas with auxiliary variables in debug mode. + + Args: + constraints (Dict): + The constraints/weights to be pretty printed. + to_obs (List[str]): + The fluents being observed. + """ aux_map = {} index = 0 for c in constraints: @@ -157,6 +247,15 @@ def _debug_aux_pprint(constraints: Dict, to_obs: List[str]): @staticmethod def _build_disorder_constraints(obs_lists: ObservationLists): + """Builds disorder constraints. Corresponds to step 1 of the AMDN algorithm. + + Args: + obs_lists (ObservationLists): + The tokens to be analyzed. + + Returns: + The disorder constraints to be used in the algorithm. + """ disorder_constraints = {} # iterate through all traces @@ -200,6 +299,15 @@ def _build_disorder_constraints(obs_lists: ObservationLists): @staticmethod def _build_hard_parallel_constraints(obs_lists: ObservationLists): + """Builds hard parallel constraints. + + Args: + obs_lists (ObservationLists): + The tokens to be analyzed. + + Returns: + The hard parallel constraints to be used in the algorithm. + """ hard_constraints = {} # create a list of all tuples for act in obs_lists.actions: @@ -211,6 +319,15 @@ def _build_hard_parallel_constraints(obs_lists: ObservationLists): @staticmethod def _build_soft_parallel_constraints(obs_lists: ObservationLists): + """Builds soft parallel constraints. + + Args: + obs_lists (ObservationLists): + The tokens to be analyzed. + + Returns: + The soft parallel constraints to be used in the algorithm. + """ soft_constraints = {} # NOTE: the paper does not take into account possible conflicts between the preconditions of actions @@ -247,7 +364,20 @@ def _build_soft_parallel_constraints(obs_lists: ObservationLists): return soft_constraints @staticmethod - def _build_parallel_constraints(obs_lists: ObservationLists, debug: int, to_obs: Optional[List[str]]): + def _build_parallel_constraints(obs_lists: ObservationLists, debug: bool, to_obs: Optional[List[str]]): + """Main driver for building parallel constraints. Corresponds to step 2 of the AMDN algorithm. + + Args: + obs_lists (ObservationLists): + The tokens that were analyzed. + debug (bool): + Optional debugging mode. + to_obs (Optional[List[str]]): + If in the optional debugging mode, the list of fluents to observe. + + Returns: + The parallel constraints. + """ hard_constraints = AMDN._build_hard_parallel_constraints(obs_lists) soft_constraints = AMDN._build_soft_parallel_constraints(obs_lists) if debug: @@ -259,6 +389,15 @@ def _build_parallel_constraints(obs_lists: ObservationLists, debug: int, to_obs: @staticmethod def _calculate_all_r_occ(obs_lists: ObservationLists): + """Calculates the total number of (true) propositions in the provided traces/tokens. + + Args: + obs_lists (ObservationLists): + The tokens to be analyzed. + + Returns: + The total number of (true) propositions in the provided traces/tokens. + """ # tracks occurrences of all propositions all_occ = 0 for trace in obs_lists: @@ -268,6 +407,17 @@ def _calculate_all_r_occ(obs_lists: ObservationLists): @staticmethod def _set_up_occurrences_dict(obs_lists: ObservationLists): + """Helper function used when constructing noise constraints. + Sets up an "occurrence" dictionary used to track the occurrences of propositions + before or after actions. + + Args: + obs_lists (ObservationLists): + The tokens to be analyzed. + + Returns: + The blank "occurrences" dictionary. + """ # set up dict occurrences = {} for a in obs_lists.actions: @@ -278,6 +428,19 @@ def _set_up_occurrences_dict(obs_lists: ObservationLists): @staticmethod def _noise_constraints_6(obs_lists: ObservationLists, all_occ: int, occ_threshold: int): + """Noise constraints (6) in the AMDN paper. + + Args: + obs_lists (ObservationLists): + The tokens that were analyzed. + all_occ (int): + The number of occurrences of all (true) propositions in the given observation list. + occ_threshold (int): + Threshold to be used for noise constraints. + + Returns: + The noise constraints. + """ noise_constraints_6 = {} occurrences = AMDN._set_up_occurrences_dict(obs_lists) @@ -303,6 +466,17 @@ def _noise_constraints_6(obs_lists: ObservationLists, all_occ: int, occ_threshol @staticmethod def _noise_constraints_7(obs_lists: ObservationLists, all_occ: int): + """Noise constraints (7) in the AMDN paper. + + Args: + obs_lists (ObservationLists): + The tokens that were analyzed. + all_occ (int): + The number of occurrences of all (true) propositions in the given observation list. + + Returns: + The noise constraints. + """ noise_constraints_7 = {} # set up dict occurrences = {} @@ -331,6 +505,19 @@ def _noise_constraints_7(obs_lists: ObservationLists, all_occ: int): @staticmethod def _noise_constraints_8(obs_lists, all_occ: int, occ_threshold: int): + """Noise constraints (8) in the AMDN paper. + + Args: + obs_lists (ObservationLists): + The tokens that were analyzed. + all_occ (int): + The number of occurrences of all (true) propositions in the given observation list. + occ_threshold (int): + Threshold to be used for noise constraints. + + Returns: + The noise constraints. + """ noise_constraints_8 = {} occurrences = AMDN._set_up_occurrences_dict(obs_lists) @@ -357,7 +544,19 @@ def _noise_constraints_8(obs_lists, all_occ: int, occ_threshold: int): return noise_constraints_8 @staticmethod - def _build_noise_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int, to_obs: Optional[List[str]]): + def _build_noise_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: bool, to_obs: Optional[List[str]]): + """Driver for building all noise constraints. Corresponds to step 3 of the AMDN algorithm. + + Args: + obs_lists (ObservationLists): + The tokens that were analyzed. + occ_threshold (int): + Threshold to be used for noise constraints. + debug (bool): + Optional debugging mode. + to_obs (Optional[List[str]]): + If in the optional debugging mode, the list of fluents to observe. + """ # calculate all occurrences for use in weights all_occ = AMDN._calculate_all_r_occ(obs_lists) nc_6 = AMDN._noise_constraints_6(obs_lists, all_occ, occ_threshold) @@ -373,7 +572,20 @@ def _build_noise_constraints(obs_lists: ObservationLists, occ_threshold: int, de return{**nc_6, **nc_7, **nc_8} @staticmethod - def _set_all_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int): + def _set_all_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: bool): + """Main driver for generating all constraints in the AMDN algorithm. + + Args: + obs_lists (ObservationLists): + The tokens that were analyzed. + occ_threshold (int): + Threshold to be used for noise constraints. + debug (bool): + Optional debugging mode. + + Returns: + A dictionary that constains all of the constraints set and all of their weights. + """ to_obs = None if debug: to_obs = AMDN._get_observe(obs_lists) @@ -386,9 +598,22 @@ def _set_all_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: return {**disorder_constraints, **parallel_constraints, **noise_constraints} @staticmethod - def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: int): - constraints = AMDN._set_all_constraints(obs_lists, occ_threshold, debug) + def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: bool): + """Returns the WCNF and the decoder according to the constraints generated. + Corresponds to step 4 of the AMDN algorithm. + + Args: + obs_lists (ObservationLists): + The tokens that were analyzed. + occ_threshold (int): + Threshold to be used for noise constraints. + debug (bool): + Optional debugging mode. + Returns: + The WCNF and corresponding decode dictionary. + """ + constraints = AMDN._set_all_constraints(obs_lists, occ_threshold, debug) # extract hard constraints hard_constraints = [] for c, weight in constraints.items(): @@ -402,6 +627,15 @@ def _solve_constraints(obs_lists: ObservationLists, occ_threshold: int, debug: i @staticmethod def _split_raw_fluent(raw_f: Hashable, learned_actions: Dict[str, LearnedAction]): + """Helper function for `_extract_model` that updates takes raw fluents to update + a dictionary of `LearnedActions`. + + Args: + raw_f (Hashable): + The raw fluent to parse. + learned_actions (Dict[str, LearnedAction]): + The dictionary of learned actions that will be used to create the model. + """ raw_f = str(raw_f)[1:-1] pre_str = " is a precondition of " add_str = " is added by " @@ -418,6 +652,18 @@ def _split_raw_fluent(raw_f: Hashable, learned_actions: Dict[str, LearnedAction] @staticmethod def _extract_model(obs_lists: ObservationLists, model: Dict[Hashable, bool]): + """Converts a raw model generated from the pysat module into a macq `Model`. + Corresponds to step 5 of the AMDN algorithm. + + Args: + obs_lists (ObservationLists): + The tokens that were analyzed. + model (Dict[Hashable, bool]): + The raw model to parse and analyze. + + Returns: + The macq action `Model`. + """ # convert the result to a Model fluents = obs_lists.propositions # set up LearnedActions