def multi_set_matrix(key, scaled_keys):
    """
    Performs a linear combination of the given keys, i.e., scales and sums all the keys in scaled_keys dict and adds
    offset if CONSTANT in scaled_keys. If the key itself is in scaled_keys, it adds to its scaled value.
    Sets the result to the given feature's future value.
    :param str key: the named key.
    :param dict scaled_keys: the dictionary containing the weights (scalars) for each named key.
    :rtype: KeyedMatrix
    :return: a matrix performing the given linear combination to the given key.
    """
    return KeyedMatrix({makeFuture(key): KeyedVector(scaled_keys)})
Example #2
0
 def _leaf_func(leaf_expr: Dict) -> KeyedMatrix:
     if _is_linear_function(leaf_expr) or self._is_constant_expr(
             leaf_expr):
         return KeyedMatrix({
             makeFuture(key):
             KeyedVector({CONSTANT: 0} if len(leaf_expr) ==
                         0 else leaf_expr)
         })
     raise NotImplementedError(
         f'Could not parse RDDL expression, got invalid subtree: "{leaf_expr}"!'
     )
Example #3
0
 def _createSensorDyn(self, human):
     for d in Directions:
         beepKey = stateKey(human.name, 'sensor_' + d.name)
         locsWithNbrs = list(self.world_map.neighbors[d.value].keys())
         tree = {
             'if':
             equalRow(makeFuture(stateKey(human.name, 'loc')),
                      locsWithNbrs),
             None:
             setToConstantMatrix(beepKey, 'none')
         }
         for il, loc in enumerate(locsWithNbrs):
             nbr = self.world_map.neighbors[d.value][loc]
             tree[il] = self._sense1Location(beepKey, nbr)
         self.world.setDynamics(beepKey, True, makeTree(tree))
Example #4
0
    def _makeMoveActions(self, agent):
        """
        N/E/S/W actions
        Legality: if current location has a neighbor in the given direction
        Dynamics: 1) change human's location; 2) set the seen flag for new location to True
        3) Set the observable victim variables to the first victim at the new location, if any
        4) Reset the crosshair/approached vars to none
        """
        self.moveActions[agent.name] = []
        locKey = stateKey(agent.name, 'loc')

        for direction in Directions:
            # Legal if current location has a neighbor in the given direction
            locsWithNbrs = set(self.neighbors[direction.value].keys())
            legalityTree = makeTree({
                'if': equalRow(locKey, locsWithNbrs),
                True: True,
                False: False
            })
            action = agent.addAction({
                'verb': 'move',
                'object': direction.name
            }, legalityTree)
            self.moveActions[agent.name].append(action)

            # Dynamics of this move action: change the agent's location to 'this' location
            lstlocsWithNbrs = list(locsWithNbrs)
            tree = {'if': equalRow(locKey, lstlocsWithNbrs)}
            for il, loc in enumerate(lstlocsWithNbrs):
                tree[il] = setToConstantMatrix(
                    locKey, self.neighbors[direction.value][loc])
            self.world.setDynamics(locKey, action, makeTree(tree))

            # move increments the counter of the location we moved to
            for dest in self.all_locations:
                destKey = stateKey(agent.name, 'locvisits_' + str(dest))
                tree = makeTree({
                    'if': equalRow(makeFuture(locKey), dest),
                    True: incrementMatrix(destKey, 1),
                    False: noChangeMatrix(destKey)
                })
                self.world.setDynamics(destKey, action, tree)

            # increment time
            self.world.setDynamics(
                self.world.time, action,
                makeTree(incrementMatrix(self.world.time, MOVE_TIME_INC)))
 def verify_constraints(self) -> None:
     """
     Verifies the constraints stored from the RDDL definition against the current state of the world.
     For each unsatisfied constraint, an `AssertionError` is thrown if `const_as_assert` parameter is `True`,
     otherwise a message is sent via `logging.info`.
     """
     # verifies if all constraints are satisfied by verifying the trees against current state
     for tree, expr in self._constraint_trees.items():
         state = self.world.state.copySubset()
         state *= tree
         val = state.marginal(makeFuture(
             _ASSERTION_KEY)).expectation()  # expected / mean value
         # value has to be > 0.5, which is truth value in PsychSim (see psychsim.world.World.float2value)
         if val <= 0.5:
             err_msg = f'State or action constraint "{expression_to_rddl(expr)}" ' \
                       f'not satisfied given current world state:\n{state}'
             if self._const_as_assert:
                 raise AssertionError(err_msg)
             logging.info(err_msg)
Example #6
0
    def _createTriageAction(self, agent, color):

        loc_key = stateKey(agent.name, 'loc')

        # legal only if any "active" victim of given color is in the same loc
        tree = {
            'if': equalRow(loc_key, self.world_map.all_locations),
            None: False
        }
        for i, loc in enumerate(self.world_map.all_locations):
            vicsInLocOfClrKey = stateKey(WORLD, 'ctr_' + loc + '_' + color)
            tree[i] = {
                'if': thresholdRow(vicsInLocOfClrKey, 0),
                True: True,
                False: False
            }
        action = agent.addAction({'verb': 'triage_' + color}, makeTree(tree))

        # different triage time thresholds according to victim type
        threshold = 7 if color == GREEN_STR else 14
        long_enough = differenceRow(makeFuture(self.world.time),
                                    self.world.time, threshold)

        # make triage dynamics for counters of each loc
        for loc in self.world_map.all_locations:
            # successful triage conditions
            conds = [equalRow(loc_key, loc), long_enough]

            # location-specific counter of vics of this color: if successful, decrement
            vicsInLocOfClrKey = stateKey(WORLD, 'ctr_' + loc + '_' + color)
            tree = makeTree(
                anding(conds, incrementMatrix(vicsInLocOfClrKey, -1),
                       noChangeMatrix(vicsInLocOfClrKey)))
            self.world.setDynamics(vicsInLocOfClrKey, action, tree)

            # white: increment
            vicsInLocOfClrKey = stateKey(WORLD, 'ctr_' + loc + '_' + WHITE_STR)
            tree = makeTree(
                anding(conds, incrementMatrix(vicsInLocOfClrKey, 1),
                       noChangeMatrix(vicsInLocOfClrKey)))
            self.world.setDynamics(vicsInLocOfClrKey, action, tree)

        # Color saved counter: increment
        saved_key = stateKey(agent.name, 'numsaved_' + color)
        tree = {
            'if': long_enough,
            True: incrementMatrix(saved_key, 1),
            False: noChangeMatrix(saved_key)
        }
        self.world.setDynamics(saved_key, action, makeTree(tree))

        # Color saved: according to difference
        diff = {makeFuture(saved_key): 1, saved_key: -1}
        saved_key = stateKey(agent.name, 'saved_' + color)
        self.world.setDynamics(saved_key, action,
                               makeTree(dynamicsMatrix(saved_key, diff)))
        self.world.setDynamics(
            saved_key, True,
            makeTree(setFalseMatrix(saved_key)))  # default: set to False

        # increment time
        self.world.setDynamics(
            self.world.time, action,
            makeTree(incrementMatrix(self.world.time, threshold)))

        self.triageActs[agent.name][color] = action
Example #7
0
    def _convert_variable_expr(self,
                               expression: Expression,
                               param_map: Dict[str, str] = None,
                               dependencies: Set[str] = None) -> Dict:

        name, params = expression.args
        if params is not None:
            # processes variable's parameters
            param_vals = []
            feat_idxs = {}  # stores indexes of parameters that are variables
            for i, p in enumerate(params):
                if param_map is not None and p in param_map:
                    param_vals.append([
                        param_map[p]
                    ])  # replace param placeholder with value on dict
                elif isinstance(p, str) and self._is_enum_value(p):
                    param_vals.append([p.replace('@', '')
                                       ])  # replace with enum value
                elif isinstance(p, Expression):
                    # param is a variable expression, so convert and check its type
                    p = self._convert_expression(p, param_map, dependencies)
                    assert len(p) == 1, f'Parameter is not a constant or variable name: "{p}"' \
                                        f'in RDDL expression "{expression_to_rddl(expression)}"!'
                    feat_name = next(iter(p.keys()))
                    if feat_name == CONSTANT:
                        param_vals.append([
                            p[CONSTANT]
                        ])  # if it's a constant, param equals its value
                    elif self.world.variables[feat_name]['domain'] == list:
                        # if it's a variable and has finite domain (enum or object), store type values
                        feat_idxs[feat_name] = i
                        param_vals.append(
                            self.world.variables[feat_name]['elements'])
                    else:
                        ValueError(
                            f'Unknown or infinite domain param {p} '
                            f'in RDDL expression "{expression_to_rddl(expression)}"!'
                        )
                else:
                    raise ValueError(
                        f'Unknown param {p} in RDDL expression "{expression_to_rddl(expression)}"!'
                    )

            # get combinations between parameter values
            param_combs = list(it.product(*param_vals))
            if len(param_combs) == 1:
                param_vals = tuple(
                    param_combs[0]
                )  # if only one parameter combination, move on
            else:
                # otherwise, create one (possibly nested) switch statement to contemplate all possible param values
                feat_idxs = sorted(
                    [(feat, idx) for feat, idx in feat_idxs.items()
                     if len(param_vals[idx]) > 1],
                    key=lambda feat_idx: len(param_vals[feat_idx[1]]))
                feats_case_vals = {}
                for param_comb in param_combs:
                    param_map.update(
                        dict(zip(params, param_comb)
                             ))  # update map to replace variables with values
                    expr = self._convert_expression(expression, param_map,
                                                    dependencies)
                    comb_key = tuple(param_comb[idx] for _, idx in feat_idxs)
                    feats_case_vals[
                        comb_key] = expr  # store value/expression for combination

                def _create_nested_switch(cur_feat_idx, cur_comb):
                    if cur_feat_idx == len(feat_idxs):
                        # terminal case, just return value/expression for parameter combination
                        return feats_case_vals[cur_comb]

                    # otherwise create case-branch for each feature value
                    feat, idx = feat_idxs[cur_feat_idx]
                    case_vals = param_vals[idx]
                    case_branches = []
                    for feat_val in case_vals:
                        case_branches.append(
                            _create_nested_switch(cur_feat_idx + 1,
                                                  cur_comb + (feat_val, )))
                    case_vals = [{CONSTANT: v} for v in case_vals]
                    case_vals[
                        -1] = 'default'  # replace last value with "default" case option
                    cond = {feat: 1}
                    return {
                        'switch': (cond, case_vals, case_branches)
                    }  # return a switch expression

                return _create_nested_switch(0, ())
        else:
            param_vals = (None, )
        f_name = (name, ) + param_vals

        # check if it's a named constant, return it's value
        if self._is_constant(f_name):
            value = self._get_constant_value(f_name)
            return {CONSTANT: value}

        # check if we should get future (current) or old value, from dependency list and from name
        future = '\'' in name or (dependencies is not None
                                  and name in dependencies)

        # check if this variable refers to a known feature, return the feature
        if self._is_feature(f_name):  #
            f_name = self._get_feature(f_name)
            return {makeFuture(f_name) if future else f_name: 1.}

        # check if it's an action
        ag_actions = []
        for agent in self.world.agents.values():
            if self._is_action(
                    f_name, agent
            ):  # check if this variable refers to an agent's action
                ag_actions.append((agent, self._get_action(f_name,
                                                           agent), future))
        if len(ag_actions) > 0:
            # TODO can do plane disjunction when supported in PsychSim
            # creates OR nested tree for matching any agents' actions
            or_tree = {'action': ag_actions[0]}
            for ag_action in ag_actions[1:]:
                or_tree = {'logical_or': (or_tree, {'action': ag_action})}
            return or_tree

        raise ValueError(
            f'Could not find feature, action or constant from RDDL expression '
            f'"{expression_to_rddl(expression)}"!')
Example #8
0
    def _createTriageAction(self, agent, color):

        fov_key = stateKey(agent.name, FOV_FEATURE)
        loc_key = stateKey(agent.name, 'loc')

        legal = {'if': equalRow(fov_key, color), True: True, False: False}
        action = agent.addAction({'verb': 'triage_' + color}, makeTree(legal))

        if color == GREEN_STR:
            threshold = 7
        else:
            threshold = 14
        longEnough = differenceRow(makeFuture(self.world.time),
                                   self.world.time, threshold)

        for loc in self.world_map.all_locations:
            # successful triage conditions
            conds = [
                equalRow(fov_key, color),
                equalRow(loc_key, loc), longEnough
            ]

            # location-specific counter of vics of this color: if successful, decrement
            vicsInLocOfClrKey = stateKey(WORLD, 'ctr_' + loc + '_' + color)
            tree = makeTree(
                anding(conds, incrementMatrix(vicsInLocOfClrKey, -1),
                       noChangeMatrix(vicsInLocOfClrKey)))
            self.world.setDynamics(vicsInLocOfClrKey, action, tree)

            # white: increment
            vicsInLocOfClrKey = stateKey(WORLD, 'ctr_' + loc + '_' + WHITE_STR)
            tree = makeTree(
                anding(conds, incrementMatrix(vicsInLocOfClrKey, 1),
                       noChangeMatrix(vicsInLocOfClrKey)))
            self.world.setDynamics(vicsInLocOfClrKey, action, tree)

        # Fov update to white
        tree = {
            'if': longEnough,
            True: setToConstantMatrix(fov_key, WHITE_STR),
            False: noChangeMatrix(fov_key)
        }
        self.world.setDynamics(fov_key, action, makeTree(tree))

        # Color saved counter: increment
        saved_key = stateKey(agent.name, 'numsaved_' + color)
        tree = {
            'if': longEnough,
            True: incrementMatrix(saved_key, 1),
            False: noChangeMatrix(saved_key)
        }
        self.world.setDynamics(saved_key, action, makeTree(tree))

        # Color saved: according to difference
        diff = {makeFuture(saved_key): 1, saved_key: -1}
        saved_key = stateKey(agent.name, 'saved_' + color)
        self.world.setDynamics(saved_key, action,
                               makeTree(dynamicsMatrix(saved_key, diff)))
        self.world.setDynamics(
            saved_key, True,
            makeTree(setFalseMatrix(saved_key)))  # default: set to False

        # increment time
        self.world.setDynamics(
            self.world.time, action,
            makeTree(incrementMatrix(self.world.time, threshold)))

        self.triageActs[agent.name][color] = action
Example #9
0
    def _get_plane(self,
                   expr: Dict,
                   negate: bool = False) -> KeyedPlane or None:
        # signature: KeyedPlane(KeyedVector(weights), threshold, comparison)

        if 'not' in expr and len(expr) == 1:
            # if NOT, get negated operation
            return self._get_plane(expr['not'], negate=not negate)

        if _is_linear_function(expr):
            # assumes linear combination of all features in vector has to be > 0.5,
            # which is truth value in PsychSim (see psychsim.world.World.float2value)
            return KeyedPlane(KeyedVector(expr), 0.5 + EPS, -1) if negate else \
                KeyedPlane(KeyedVector(expr), 0.5, 1)

        if 'action' in expr and len(expr['action']) == 3:
            # conditional on specific agent's action (agent's action == the action)
            agent, action, future = expr['action']
            key = makeFuture(actionKey(agent.name)) if future else actionKey(
                agent.name)  # check future vs prev action
            if negate:
                return KeyedPlane(KeyedVector({key: 1}),
                                  action, 1) | KeyedPlane(
                                      KeyedVector({key: 1}), action, -1)
            else:
                return KeyedPlane(KeyedVector({key: 1}), action, 0)

        if 'linear_and' in expr and len(expr) == 1:
            # AND of features (sum > w_sum - 0.5), see psychsim.world.World.float2value
            # for negation, ~(A ^ B ^ ...) <=> ~A | ~B | ...
            expr = expr['linear_and']
            if negate:
                return self._get_plane(
                    {'linear_or': _negate_linear_function(expr)})
            else:
                return KeyedPlane(
                    KeyedVector(expr),
                    sum([v for v in expr.values() if v > 0]) - 0.5, 1)

        if 'linear_or' in expr and len(expr) == 1:
            # OR of features (A | B | ...) <=> ~(~A ^ ~B ^ ...)
            # for negation, ~(A | B | ...) <=> ~A ^ ~B ^ ...
            expr = expr['linear_or']
            if negate:
                return self._get_plane(
                    {'linear_and': _negate_linear_function(expr)})
            else:
                expr = _negate_linear_function(expr)
                return KeyedPlane(
                    KeyedVector(expr),
                    sum([v for v in expr.values() if v > 0]) - 0.5 + EPS, -1)

        if 'logic_and' in expr and len(expr) == 1:
            # get conjunction between sub-expressions (all planes must be valid)
            # for negation, ~(A ^ B ^ ...) <=> ~A | ~B |...
            sub_exprs = expr['logic_and']
            if negate:
                planes = [
                    self._get_plane({'not': sub_expr})
                    for sub_expr in sub_exprs
                ]
                invalid = None in planes or any(
                    len(plane.planes) > 1 and plane.isConjunction
                    for plane in planes)
                return None if invalid else reduce(lambda p1, p2: p1 | p2,
                                                   planes)  # disjunction
            else:
                planes = [self._get_plane(sub_expr) for sub_expr in sub_exprs]
                invalid = None in planes or any(
                    len(plane.planes) > 1 and not plane.isConjunction
                    for plane in planes)
                return None if invalid else reduce(lambda p1, p2: p1 & p2,
                                                   planes)  # conjunction

        if 'logic_or' in expr and len(expr) == 1:
            # get disjunction between sub-expressions (all planes must be valid)
            # for negation, ~(A | B | ...) <=> ~A ^ ~B ^ ...
            sub_exprs = expr['logic_or']
            if negate:
                planes = [
                    self._get_plane({'not': sub_expr})
                    for sub_expr in sub_exprs
                ]
                invalid = None in planes or any(
                    len(plane.planes) > 1 and not plane.isConjunction
                    for plane in planes)
                return None if invalid else reduce(lambda p1, p2: p1 & p2,
                                                   planes)  # conjunction
            else:
                planes = [self._get_plane(sub_expr) for sub_expr in sub_exprs]
                invalid = None in planes or any(
                    len(plane.planes) > 1 and plane.isConjunction
                    for plane in planes)
                return None if invalid else reduce(lambda p1, p2: p1 | p2,
                                                   planes)  # disjunction

        # test binary operators
        op = next(iter(expr.keys()))
        if len(expr) == 1 and op in {
                'eq', 'neq', 'gt', 'lt', 'geq', 'leq', 'imply'
        }:
            lhs, rhs = expr[op]
            if (not negate and 'eq' in expr) or (negate and 'neq' in expr):
                # takes equality of pwl comb in vectors (difference==0 or expr==threshold)
                weights, thresh = self._get_relational_plane_thresh(lhs, rhs)
                return KeyedPlane(KeyedVector(weights), thresh, 0)

            if (not negate and 'neq' in expr) or (negate and 'eq' in expr):
                # takes equality of pwl comb in vectors (difference!=0 or expr!=threshold)
                weights, thresh = self._get_relational_plane_thresh(lhs, rhs)
                return KeyedPlane(KeyedVector(weights),
                                  thresh, 1) | KeyedPlane(
                                      KeyedVector(weights), thresh, -1)

            if (not negate and 'gt' in expr) or (negate and 'leq' in expr):
                # takes diff of pwl comb in vectors (difference>0 or expr>threshold)
                weights, thresh = self._get_relational_plane_thresh(lhs, rhs)
                return KeyedPlane(KeyedVector(weights), thresh, 1)

            if (not negate and 'lt' in expr) or (negate and 'geq' in expr):
                # takes diff of pwl comb in vectors (difference<0 or expr<threshold)
                weights, thresh = self._get_relational_plane_thresh(lhs, rhs)
                return KeyedPlane(KeyedVector(weights), thresh, -1)

            if (not negate and 'geq' in expr) or (negate and 'lt' in expr):
                # takes diff of pwl comb in vectors (difference>=0 or expr>=threshold)
                weights, thresh = self._get_relational_plane_thresh(lhs, rhs)
                if isinstance(thresh, str):
                    return KeyedPlane(KeyedVector(weights),
                                      thresh, 1) | KeyedPlane(
                                          KeyedVector(weights), thresh, 0)
                else:
                    return KeyedPlane(KeyedVector(weights), thresh - EPS, 1)

            if (not negate and 'leq' in expr) or (negate and 'gt' in expr):
                # takes diff of pwl comb in vectors (difference<=0 or expr<=threshold)
                weights, thresh = self._get_relational_plane_thresh(lhs, rhs)
                if isinstance(thresh, str):
                    KeyedPlane(KeyedVector(weights), thresh, -1) | KeyedPlane(
                        KeyedVector(weights), thresh, 0)
                else:
                    return KeyedPlane(KeyedVector(weights), thresh + EPS, -1)

            if 'imply' in expr:
                # if IMPLICATION, false only if left is true and right is false, ie
                # true if left is false or right is true
                if not (_is_linear_function(lhs) and _is_linear_function(rhs)):
                    return None  # both operands have to be linear combinations
                if negate:
                    return KeyedPlane(KeyedVector(lhs), 0.5, 1) & KeyedPlane(
                        KeyedVector(rhs), 0.5 + EPS, -1)
                else:
                    return KeyedPlane(KeyedVector(lhs),
                                      0.5 + EPS, -1) | KeyedPlane(
                                          KeyedVector(rhs), 0.5, 1)

        return None