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)})
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}"!' )
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))
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)
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
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)}"!')
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
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