def adjacent(self, i, j): _min_i, _max_i = self.unit_bounding_box(i, True) _min_j, _max_j = self.unit_bounding_box(j, True) if _min_i is None or _min_j is None: _ri, _rj = self.unit_polytope(i), self.unit_polytope(j) return pt.is_adjacent(_ri, _rj) else: return np.all(_min_i <= _max_j) and np.all(_min_j <= _max_i)
def find_adjacent_regions(partition): """Return region pairs that are spatially adjacent. @type partition: iterable container of L{Region} @rtype: lil_matrix """ n = len(partition) adj = sp.lil_matrix((n, n), dtype=np.int8) s = partition.regions for i, a in enumerate(s): adj[i, i] = 1 for j, b in enumerate(s[0:i]): adj[i, j] = adj[j, i] = pc.is_adjacent(a, b) return adj
def add_grid(ppp, grid_size=None, num_grid_pnts=None, abs_tol=1e-10): """ This function takes a proposition preserving partition ppp and the size of the grid or the number of grids, and returns a refined proposition preserving partition with grids. Input: - `ppp`: a L{PropPreservingPartition} object - `grid_size`: the size of the grid, type: float or list of float - `num_grid_pnts`: the number of grids for each dimension, type: integer or list of integer Output: - A L{PropPreservingPartition} object with grids Note: There could be numerical instabilities when the continuous propositions in ppp do not align well with the grid resulting in very small regions. Performace significantly degrades without glpk. """ if (grid_size != None) & (num_grid_pnts != None): raise Exception("add_grid: Only one of the grid size or number of \ grid points parameters is allowed to be given.") if (grid_size == None) & (num_grid_pnts == None): raise Exception("add_grid: At least one of the grid size or number of \ grid points parameters must be given.") dim = len(ppp.domain.A[0]) domain_bb = ppp.domain.bounding_box size_list = list() if grid_size != None: if isinstance(grid_size, list): if len(grid_size) == dim: size_list = grid_size else: raise Exception( "add_grid: grid_size isn't given in a correct format.") elif isinstance(grid_size, float): for i in xrange(dim): size_list.append(grid_size) else: raise Exception("add_grid: " "grid_size isn't given in a correct format.") else: if isinstance(num_grid_pnts, list): if len(num_grid_pnts) == dim: for i in xrange(dim): if isinstance(num_grid_pnts[i], int): grid_size = (float(domain_bb[1][i]) - float(domain_bb[0][i])) / num_grid_pnts[i] size_list.append(grid_size) else: raise Exception( "add_grid: " "num_grid_pnts isn't given in a correct format.") else: raise Exception( "add_grid: " "num_grid_pnts isn't given in a correct format.") elif isinstance(num_grid_pnts, int): for i in xrange(dim): grid_size = (float(domain_bb[1][i]) - float(domain_bb[0][i])) / num_grid_pnts size_list.append(grid_size) else: raise Exception("add_grid: " "num_grid_pnts isn't given in a correct format.") j = 0 list_grid = dict() while j < dim: list_grid[j] = compute_interval(float(domain_bb[0][j]), float(domain_bb[1][j]), size_list[j], abs_tol) if j > 0: if j == 1: re_list = list_grid[j - 1] re_list = product_interval(re_list, list_grid[j]) else: re_list = product_interval(re_list, list_grid[j]) j += 1 new_list = [] parent = [] for i in xrange(len(re_list)): temp_list = list() j = 0 while j < dim * 2: temp_list.append([re_list[i][j], re_list[i][j + 1]]) j = j + 2 for j in xrange(len(ppp.regions)): tmp = pc.box2poly(temp_list) isect = tmp.intersect(ppp.regions[j], abs_tol) #if pc.is_fulldim(isect): rc, xc = pc.cheby_ball(isect) if rc > abs_tol / 2: if rc < abs_tol: print("Warning: " "One of the regions in the refined PPP is too small" ", this may cause numerical problems") if len(isect) == 0: isect = pc.Region([isect], []) isect.props = ppp.regions[j].props.copy() new_list.append(isect) parent.append(j) adj = sp.lil_matrix((len(new_list), len(new_list)), dtype=np.int8) for i in xrange(len(new_list)): adj[i, i] = 1 for j in xrange(i + 1, len(new_list)): if (ppp.adj[parent[i], parent[j]] == 1) or \ (parent[i] == parent[j]): if pc.is_adjacent(new_list[i], new_list[j]): adj[i, j] = 1 adj[j, i] = 1 return PropPreservingPartition(domain=ppp.domain, regions=new_list, adj=adj, prop_regions=ppp.prop_regions)
def pwa_partition(pwa_sys, ppp, abs_tol=1e-5): """This function takes: - a piecewise affine system C{pwa_sys} and - a proposition-preserving partition C{ppp} whose domain is a subset of the domain of C{pwa_sys} and returns a *refined* proposition preserving partition where in each region a unique subsystem of pwa_sys is active. Reference ========= Modified from Petter Nilsson's code implementing merge algorithm in: Nilsson et al. `Temporal Logic Control of Switched Affine Systems with an Application in Fuel Balancing`, ACC 2012. See Also ======== L{discretize} @type pwa_sys: L{hybrid.PwaSysDyn} @type ppp: L{PropPreservingPartition} @return: new partition and associated maps: - new partition C{new_ppp} - map of C{new_ppp.regions} to C{pwa_sys.list_subsys} - map of C{new_ppp.regions} to C{ppp.regions} @rtype: C{(L{PropPreservingPartition}, list, list)} """ if pc.is_fulldim(ppp.domain.diff(pwa_sys.domain)): raise Exception('pwa system is not defined everywhere ' + 'in state space') # for each subsystem's domain, cut it into pieces # each piece is the intersection with # a unique Region in ppp.regions new_list = [] subsys_list = [] parents = [] for i, subsys in enumerate(pwa_sys.list_subsys): for j, region in enumerate(ppp.regions): isect = region.intersect(subsys.domain) if pc.is_fulldim(isect): rc, xc = pc.cheby_ball(isect) if rc < abs_tol: msg = 'One of the regions in the refined PPP is ' msg += 'too small, this may cause numerical problems' warnings.warn(msg) # not Region yet, but Polytope ? if len(isect) == 0: isect = pc.Region([isect]) # label with AP isect.props = region.props.copy() # store new Region new_list.append(isect) # keep track of original Region in ppp.regions parents.append(j) # index of subsystem active within isect subsys_list.append(i) # compute spatial adjacency matrix n = len(new_list) adj = sp.lil_matrix((n, n), dtype=np.int8) for i, ri in enumerate(new_list): pi = parents[i] for j, rj in enumerate(new_list[0:i]): pj = parents[j] if (ppp.adj[pi, pj] == 1) or (pi == pj): if pc.is_adjacent(ri, rj): adj[i, j] = 1 adj[j, i] = 1 adj[i, i] = 1 new_ppp = PropPreservingPartition(domain=ppp.domain, regions=new_list, adj=adj, prop_regions=ppp.prop_regions) return (new_ppp, subsys_list, parents)
def add_collection(self, unit_regions, label=None): # check if already existing if label in self.index: if 1 < len(self.index): raise RuntimeError( 'cannot overwrite a collection if other collections have already been defined' ) self.__reset__() # i0 = len(self.unit_region) self.unit_region += list(unit_regions) # first group overlapping unit regions in the collection current_index = max(self.group.keys()) + 1 if self.group else 0 not_an_index = -1 n = len(unit_regions) assignment = np.full(n, not_an_index, dtype=int) groups = dict() for i in range(n): region_i = unit_regions[i] if isinstance(region_i, pt.Polytope): region_i = pt.Region([region_i]) _min_i = _max_i = None elif isinstance(region_i, pt.Region): _min_i = _max_i = None else: #if isinstance(region_i, (tuple, list)): _min_i, _max_i = region_i region_i = None group_with = set() if assignment[i] == not_an_index: group_index = current_index group = set([i0 + i]) else: group_index = assignment[i] group_with.add(group_index) group = groups[group_index] assert i0 + i in group for j in range(i + 1, n): if i0 + j in group: continue region_j = unit_regions[j] if isinstance(region_j, (pt.Polytope, pt.Region)): if region_i is None: region_i = pt.box2poly(list(zip(_min_i, _max_i))) self._unit_polytope[i0 + i] = region_i i_and_j_are_adjacent = pt.is_adjacent(region_i, region_j) else: #if isinstance(region_j, (tuple, list)): _min_j, _max_j = region_j if _min_i is None: region_j = pt.box2poly(list(zip(_min_j, _max_j))) self._unit_polytope[i0 + j] = region_j i_and_j_are_adjacent = pt.is_adjacent( region_i, region_j) else: i_and_j_are_adjacent = np.all( _min_i <= _max_j) and np.all(_min_j <= _max_i) if i_and_j_are_adjacent: if assignment[j] == not_an_index: group.add(i0 + j) else: other_group_index = assignment[j] group_with.add(other_group_index) group |= groups.pop(other_group_index) if group_with: group_index = min(group_with) else: current_index += 1 groups[group_index] = group group = np.array(list(group)) - i0 # indices in `assignment` assignment[group] = group_index # # merge the new and existing groups together for g in list(groups.keys()): adjacent = set() for h in self.group: g_and_h_are_adjacent = False for i in groups[g]: for j in self.group[h]: if self.adjacent(i, j): g_and_h_are_adjacent = True break if g_and_h_are_adjacent: adjacent.add(h) break if adjacent: h = min(adjacent) for i in adjacent - {h}: self.group[h] |= self.group.pop(i) self.group[h] |= groups.pop(g) assignment[assignment == g] = h if groups: self.group.update(groups) # if label is None: label = '' self.index[label] = assignment # self.reverse_index = np.c_[ np.repeat(np.arange(len(self.index) ), [len(self.index[s]) for s in self.index]), np.concatenate([np.arange(len(self.index[s])) for s in self.index])]
def add_grid(ppp, grid_size=None, num_grid_pnts=None, abs_tol=1e-10): """ This function takes a proposition preserving partition ppp and the size of the grid or the number of grids, and returns a refined proposition preserving partition with grids. Input: - `ppp`: a L{PropPreservingPartition} object - `grid_size`: the size of the grid, type: float or list of float - `num_grid_pnts`: the number of grids for each dimension, type: integer or list of integer Output: - A L{PropPreservingPartition} object with grids Note: There could be numerical instabilities when the continuous propositions in ppp do not align well with the grid resulting in very small regions. Performace significantly degrades without glpk. """ if (grid_size!=None)&(num_grid_pnts!=None): raise Exception("add_grid: Only one of the grid size or number of \ grid points parameters is allowed to be given.") if (grid_size==None)&(num_grid_pnts==None): raise Exception("add_grid: At least one of the grid size or number of \ grid points parameters must be given.") dim=len(ppp.domain.A[0]) domain_bb = ppp.domain.bounding_box size_list=list() if grid_size!=None: if isinstance( grid_size, list ): if len(grid_size) == dim: size_list=grid_size else: raise Exception( "add_grid: grid_size isn't given in a correct format." ) elif isinstance( grid_size, float ): for i in xrange(dim): size_list.append(grid_size) else: raise Exception("add_grid: " "grid_size isn't given in a correct format.") else: if isinstance( num_grid_pnts, list ): if len(num_grid_pnts) == dim: for i in xrange(dim): if isinstance( num_grid_pnts[i], int ): grid_size=( float(domain_bb[1][i]) -float(domain_bb[0][i]) ) /num_grid_pnts[i] size_list.append(grid_size) else: raise Exception("add_grid: " "num_grid_pnts isn't given in a correct format.") else: raise Exception("add_grid: " "num_grid_pnts isn't given in a correct format.") elif isinstance( num_grid_pnts, int ): for i in xrange(dim): grid_size=( float(domain_bb[1][i])-float(domain_bb[0][i]) ) /num_grid_pnts size_list.append(grid_size) else: raise Exception("add_grid: " "num_grid_pnts isn't given in a correct format.") j=0 list_grid=dict() while j<dim: list_grid[j] = compute_interval( float(domain_bb[0][j]), float(domain_bb[1][j]), size_list[j], abs_tol ) if j>0: if j==1: re_list=list_grid[j-1] re_list=product_interval(re_list, list_grid[j]) else: re_list=product_interval(re_list, list_grid[j]) j+=1 new_list = [] parent = [] for i in xrange(len(re_list)): temp_list=list() j=0 while j<dim*2: temp_list.append([re_list[i][j],re_list[i][j+1]]) j=j+2 for j in xrange(len(ppp.regions)): tmp = pc.box2poly(temp_list) isect = tmp.intersect(ppp.regions[j], abs_tol) #if pc.is_fulldim(isect): rc, xc = pc.cheby_ball(isect) if rc > abs_tol/2: if rc < abs_tol: print("Warning: " "One of the regions in the refined PPP is too small" ", this may cause numerical problems") if len(isect) == 0: isect = pc.Region([isect], []) isect.props = ppp.regions[j].props.copy() new_list.append(isect) parent.append(j) adj = sp.lil_matrix((len(new_list), len(new_list)), dtype=np.int8) for i in xrange(len(new_list)): adj[i,i] = 1 for j in xrange(i+1, len(new_list)): if (ppp.adj[parent[i], parent[j]] == 1) or \ (parent[i] == parent[j]): if pc.is_adjacent(new_list[i], new_list[j]): adj[i,j] = 1 adj[j,i] = 1 return PropPreservingPartition( domain = ppp.domain, regions = new_list, adj = adj, prop_regions = ppp.prop_regions )
def pwa_partition(pwa_sys, ppp, abs_tol=1e-5): """This function takes: - a piecewise affine system C{pwa_sys} and - a proposition-preserving partition C{ppp} whose domain is a subset of the domain of C{pwa_sys} and returns a *refined* proposition preserving partition where in each region a unique subsystem of pwa_sys is active. Reference ========= Modified from Petter Nilsson's code implementing merge algorithm in: Nilsson et al. `Temporal Logic Control of Switched Affine Systems with an Application in Fuel Balancing`, ACC 2012. See Also ======== L{discretize} @type pwa_sys: L{hybrid.PwaSysDyn} @type ppp: L{PropPreservingPartition} @return: new partition and associated maps: - new partition C{new_ppp} - map of C{new_ppp.regions} to C{pwa_sys.list_subsys} - map of C{new_ppp.regions} to C{ppp.regions} @rtype: C{(L{PropPreservingPartition}, list, list)} """ if pc.is_fulldim(ppp.domain.diff(pwa_sys.domain) ): raise Exception('pwa system is not defined everywhere ' + 'in state space') # for each subsystem's domain, cut it into pieces # each piece is the intersection with # a unique Region in ppp.regions new_list = [] subsys_list = [] parents = [] for i, subsys in enumerate(pwa_sys.list_subsys): for j, region in enumerate(ppp.regions): isect = region.intersect(subsys.domain) if pc.is_fulldim(isect): rc, xc = pc.cheby_ball(isect) if rc < abs_tol: msg = 'One of the regions in the refined PPP is ' msg += 'too small, this may cause numerical problems' warnings.warn(msg) # not Region yet, but Polytope ? if len(isect) == 0: isect = pc.Region([isect]) # label with AP isect.props = region.props.copy() # store new Region new_list.append(isect) # keep track of original Region in ppp.regions parents.append(j) # index of subsystem active within isect subsys_list.append(i) # compute spatial adjacency matrix n = len(new_list) adj = sp.lil_matrix((n, n), dtype=np.int8) for i, ri in enumerate(new_list): pi = parents[i] for j, rj in enumerate(new_list[0:i]): pj = parents[j] if (ppp.adj[pi, pj] == 1) or (pi == pj): if pc.is_adjacent(ri, rj): adj[i, j] = 1 adj[j, i] = 1 adj[i, i] = 1 new_ppp = PropPreservingPartition( domain = ppp.domain, regions = new_list, adj = adj, prop_regions = ppp.prop_regions ) return (new_ppp, subsys_list, parents)
def compute_adj(self): """Update the adjacency matrix by checking all region pairs. Uses L{polytope.is_adjacent}. """ n = len(self.regions) adj = sp.lil_matrix((n, n)) logger.info('computing adjacency from scratch...') for i, region0 in enumerate(self.regions): for j, region1 in enumerate(self.regions): if i == j: adj[i, j] = 1 continue if pc.is_adjacent(region0, region1): adj[i, j] = 1 adj[j, i] = 1 logger.info('regions: ' + str(i) + ', ' + str(j) + ', are adjacent.') logger.info('...done computing adjacency.') # check previous one to unmask errors if self.adj is not None: logger.info('checking previous adjacency...') ok = True row, col = adj.nonzero() for i, j in zip(row, col): assert(adj[i, j]) if adj[i, j] != self.adj[i, j]: ok = False msg = 'PPP adjacency matrix is incomplete, ' msg += 'missing: (' + str(i) + ', ' + str(j) + ')' logger.error(msg) row, col = self.adj.nonzero() for i, j in zip(row, col): assert(self.adj[i, j]) if adj[i, j] != self.adj[i, j]: ok = False msg = 'PPP adjacency matrix is incorrect, ' msg += 'has 1 at: (' + str(i) + ', ' + str(j) + ')' logger.error(msg) if not ok: logging.error('PPP had incorrect adjacency matrix.') logger.info('done checking previous adjacency.') else: ok = True logger.info('no previous adjacency found: ' + 'skip verification.') # update adjacency self.adj = adj return ok
def pwa_shrunk_partition(pwa_sys, ppp, eps, abs_tol=1e-5): """This function takes: - a piecewise affine system C{pwa_sys} and - a proposition-preserving partition C{ppp} whose domain is a subset of the domain of C{pwa_sys} - a shrinkage factor C{eps} and returns a *refined* proposition preserving partition where in each region a unique subsystem of pwa_sys is active and pwa_sys domains are shrunk to account for estimation errors. See Also ======== L{pwa_partition} @type pwa_sys: L{hybrid.PwaSysDyn} @type ppp: L{PropPreservingPartition} @return: new partition and associated maps: - new partition C{new_ppp} - map of C{new_ppp.regions} to C{pwa_sys.list_subsys} - map of C{new_ppp.regions} to C{ppp.regions} @rtype: C{(L{PropPreservingPartition}, list, list)} """ new_list = [] subsys_list = [] parents = [] for i, subsys in enumerate(pwa_sys.list_subsys): dom = shrinkPoly(subsys.domain, eps) for j, region in enumerate(ppp.regions): isect = pc.reduce(region.intersect(dom)) if pc.is_fulldim(isect): rc, xc = pc.cheby_ball(isect) if rc < abs_tol: msg = 'One of the regions in the refined PPP is ' msg += 'too small, this may cause numerical problems' warnings.warn(msg) # not Region yet, but Polytope ? if len(isect) == 0: isect = pc.Region([isect]) # label with AP isect.props = region.props.copy() # store new Region new_list.append(isect) # keep track of original Region in ppp.regions parents.append(j) # index of subsystem active within isect subsys_list.append(i) # compute spatial adjacency matrix n = len(new_list) adj = sp.lil_matrix((n, n), dtype=np.int8) for i, ri in enumerate(new_list): pi = parents[i] for j, rj in enumerate(new_list[0:i]): pj = parents[j] if (ppp.adj[pi, pj] == 1) or (pi == pj): # account for shrinkage in adjacency check if pc.is_adjacent(ri, rj, 2*eps): adj[i, j] = 1 adj[j, i] = 1 adj[i, i] = 1 new_ppp = PropPreservingPartition( domain = ppp.domain, regions = new_list, adj = adj, prop_regions = ppp.prop_regions ) return (new_ppp, subsys_list, parents)
def pwa_shrunk_partition(pwa_sys, ppp, eps, abs_tol=1e-5): """This function takes: - a piecewise affine system C{pwa_sys} and - a proposition-preserving partition C{ppp} whose domain is a subset of the domain of C{pwa_sys} - a shrinkage factor C{eps} and returns a *refined* proposition preserving partition where in each region a unique subsystem of pwa_sys is active and pwa_sys domains are shrunk to account for estimation errors. See Also ======== L{pwa_partition} @type pwa_sys: L{hybrid.PwaSysDyn} @type ppp: L{PropPreservingPartition} @return: new partition and associated maps: - new partition C{new_ppp} - map of C{new_ppp.regions} to C{pwa_sys.list_subsys} - map of C{new_ppp.regions} to C{ppp.regions} @rtype: C{(L{PropPreservingPartition}, list, list)} """ new_list = [] subsys_list = [] parents = [] for i, subsys in enumerate(pwa_sys.list_subsys): dom = shrinkPoly(subsys.domain, eps) for j, region in enumerate(ppp.regions): isect = pc.reduce(region.intersect(dom)) if pc.is_fulldim(isect): rc, xc = pc.cheby_ball(isect) if rc < abs_tol: msg = 'One of the regions in the refined PPP is ' msg += 'too small, this may cause numerical problems' warnings.warn(msg) # not Region yet, but Polytope ? if len(isect) == 0: isect = pc.Region([isect]) # label with AP isect.props = region.props.copy() # store new Region new_list.append(isect) # keep track of original Region in ppp.regions parents.append(j) # index of subsystem active within isect subsys_list.append(i) # compute spatial adjacency matrix n = len(new_list) adj = sp.lil_matrix((n, n), dtype=np.int8) for i, ri in enumerate(new_list): pi = parents[i] for j, rj in enumerate(new_list[0:i]): pj = parents[j] if (ppp.adj[pi, pj] == 1) or (pi == pj): # account for shrinkage in adjacency check if pc.is_adjacent(ri, rj, 2 * eps): adj[i, j] = 1 adj[j, i] = 1 adj[i, i] = 1 new_ppp = PropPreservingPartition(domain=ppp.domain, regions=new_list, adj=adj, prop_regions=ppp.prop_regions) return (new_ppp, subsys_list, parents)
def discretize( part, ssys, N=10, min_cell_volume=0.1, closed_loop=True, conservative=False, max_num_poly=5, use_all_horizon=False, trans_length=1, remove_trans=False, abs_tol=1e-7, plotit=False, save_img=False, cont_props=None, plot_every=1 ): """Refine the partition and establish transitions based on reachability analysis. Reference ========= U{[NOTM12] <https://tulip-control.sourceforge.io/doc/bibliography.html#notm12>} See Also ======== L{prop2partition.pwa_partition}, L{prop2partition.part2convex} @param part: L{PropPreservingPartition} object @param ssys: L{LtiSysDyn} or L{PwaSysDyn} object @param N: horizon length @param min_cell_volume: the minimum volume of cells in the resulting partition. @param closed_loop: boolean indicating whether the `closed loop` algorithm should be used. default True. @param conservative: if true, force sequence in reachability analysis to stay inside starting cell. If false, safety is ensured by keeping the sequence inside a convexified version of the original proposition preserving cell. @param max_num_poly: maximum number of polytopes in a region to use in reachability analysis. @param use_all_horizon: in closed loop algorithm: if we should look for reachability also in less than N steps. @param trans_length: the number of polytopes allowed to cross in a transition. a value of 1 checks transitions only between neighbors, a value of 2 checks neighbors of neighbors and so on. @param remove_trans: if True, remove found transitions between non-neighbors. @param abs_tol: maximum volume for an "empty" polytope @param plotit: plot partitioning as it evolves @type plotit: boolean, default = False @param save_img: save snapshots of partitioning to PDF files, requires plotit=True @type save_img: boolean, default = False @param cont_props: continuous propositions to plot @type cont_props: list of C{Polytope} @rtype: L{AbstractPwa} """ start_time = os.times()[0] orig_ppp = part min_cell_volume = (min_cell_volume /np.finfo(np.double).eps *np.finfo(np.double).eps) ispwa = isinstance(ssys, PwaSysDyn) islti = isinstance(ssys, LtiSysDyn) if ispwa: (part, ppp2pwa, part2orig) = pwa_partition(ssys, part) else: part2orig = range(len(part)) # Save original polytopes, require them to be convex if conservative: orig_list = None orig = [0] else: (part, new2old) = part2convex(part) # convexify part2orig = [part2orig[i] for i in new2old] # map new regions to pwa subsystems if ispwa: ppp2pwa = [ppp2pwa[i] for i in new2old] remove_trans = False # already allowed in nonconservative orig_list = [] for poly in part: if len(poly) == 0: orig_list.append(poly.copy()) elif len(poly) == 1: orig_list.append(poly[0].copy()) else: raise Exception("discretize: " "problem in convexification") orig = list(range(len(orig_list))) # Cheby radius of disturbance set # (defined within the loop for pwa systems) if islti: if len(ssys.E) > 0: rd = ssys.Wset.chebR else: rd = 0. # Initialize matrix for pairs to check IJ = part.adj.copy() IJ = IJ.todense() IJ = np.array(IJ) logger.debug("\n Starting IJ: \n" + str(IJ) ) # next line omitted in discretize_overlap IJ = reachable_within(trans_length, IJ, np.array(part.adj.todense()) ) # Initialize output num_regions = len(part) transitions = np.zeros( [num_regions, num_regions], dtype = int ) sol = deepcopy(part.regions) adj = part.adj.copy() adj = adj.todense() adj = np.array(adj) # next 2 lines omitted in discretize_overlap if ispwa: subsys_list = list(ppp2pwa) else: subsys_list = None ss = ssys # init graphics if plotit: try: import matplotlib.pyplot as plt plt.ion() fig, (ax1, ax2) = plt.subplots(1, 2) ax1.axis('scaled') ax2.axis('scaled') file_extension = 'pdf' except: logger.error('failed to import matplotlib') plt = None else: plt = None iter_count = 0 # List of how many "new" regions # have been created for each region # and a list of original number of neighbors #num_new_reg = np.zeros(len(orig_list)) #num_orig_neigh = np.sum(adj, axis=1).flatten() - 1 progress = list() # Do the abstraction while np.sum(IJ) > 0: ind = np.nonzero(IJ) # i,j swapped in discretize_overlap i = ind[1][0] j = ind[0][0] IJ[j, i] = 0 si = sol[i] sj = sol[j] si_tmp = deepcopy(si) sj_tmp = deepcopy(sj) #num_new_reg[i] += 1 #print(num_new_reg) if ispwa: ss = ssys.list_subsys[subsys_list[i]] if len(ss.E) > 0: rd, xd = pc.cheby_ball(ss.Wset) else: rd = 0. if conservative: # Don't use trans_set trans_set = None else: # Use original cell as trans_set trans_set = orig_list[orig[i]] S0 = solve_feasible( si, sj, ss, N, closed_loop, use_all_horizon, trans_set, max_num_poly ) msg = '\n Working with partition cells: ' + str(i) + ', ' + str(j) logger.info(msg) msg = '\t' + str(i) +' (#polytopes = ' +str(len(si) ) +'), and:\n' msg += '\t' + str(j) +' (#polytopes = ' +str(len(sj) ) +')\n' if ispwa: msg += '\t with active subsystem: ' msg += str(subsys_list[i]) + '\n' msg += '\t Computed reachable set S0 with volume: ' msg += str(S0.volume) + '\n' logger.debug(msg) #logger.debug('si \cap s0') isect = si.intersect(S0) vol1 = isect.volume risect, xi = pc.cheby_ball(isect) #logger.debug('si \ s0') diff = si.diff(S0) vol2 = diff.volume rdiff, xd = pc.cheby_ball(diff) # if pc.is_fulldim(pc.Region([isect]).intersect(diff)): # logging.getLogger('tulip.polytope').setLevel(logging.DEBUG) # diff = pc.mldivide(si, S0, save=True) # # ax = S0.plot() # ax.axis([0.0, 1.0, 0.0, 2.0]) # ax.figure.savefig('./img/s0.pdf') # # ax = si.plot() # ax.axis([0.0, 1.0, 0.0, 2.0]) # ax.figure.savefig('./img/si.pdf') # # ax = isect.plot() # ax.axis([0.0, 1.0, 0.0, 2.0]) # ax.figure.savefig('./img/isect.pdf') # # ax = diff.plot() # ax.axis([0.0, 1.0, 0.0, 2.0]) # ax.figure.savefig('./img/diff.pdf') # # ax = isect.intersect(diff).plot() # ax.axis([0.0, 1.0, 0.0, 2.0]) # ax.figure.savefig('./img/diff_cap_isect.pdf') # # logger.error('Intersection \cap Difference != \emptyset') # # assert(False) if vol1 <= min_cell_volume: logger.warning('\t too small: si \cap Pre(sj), ' 'so discard intersection') if vol1 <= min_cell_volume and isect: logger.warning('\t discarded non-empty intersection: ' 'consider reducing min_cell_volume') if vol2 <= min_cell_volume: logger.warning('\t too small: si \ Pre(sj), so not reached it') # We don't want our partitions to be smaller than the disturbance set # Could be a problem since cheby radius is calculated for smallest # convex polytope, so if we have a region we might throw away a good # cell. if (vol1 > min_cell_volume) and (risect > rd) and \ (vol2 > min_cell_volume) and (rdiff > rd): # Make sure new areas are Regions and add proposition lists if len(isect) == 0: isect = pc.Region([isect], si.props) else: isect.props = si.props.copy() if len(diff) == 0: diff = pc.Region([diff], si.props) else: diff.props = si.props.copy() # replace si by intersection (single state) isect_list = pc.separate(isect) sol[i] = isect_list[0] # cut difference into connected pieces difflist = pc.separate(diff) difflist += isect_list[1:] n_isect = len(isect_list) -1 num_new = len(difflist) # add each piece, as a new state for region in difflist: sol.append(region) # keep track of PWA subsystems map to new states if ispwa: subsys_list.append(subsys_list[i]) n_cells = len(sol) new_idx = range(n_cells-1, n_cells-num_new-1, -1) """Update transition matrix""" transitions = np.pad(transitions, (0,num_new), 'constant') transitions[i, :] = np.zeros(n_cells) for r in new_idx: #transitions[:, r] = transitions[:, i] # All sets reachable from start are reachable from both part's # except possibly the new part transitions[i, r] = 0 transitions[j, r] = 0 # sol[j] is reachable from intersection of sol[i] and S0 if i != j: transitions[j, i] = 1 # sol[j] is reachable from each piece os S0 \cap sol[i] #for k in range(n_cells-n_isect-2, n_cells): # transitions[j, k] = 1 """Update adjacency matrix""" old_adj = np.nonzero(adj[i, :])[0] # reset new adjacencies adj[i, :] = np.zeros([n_cells -num_new]) adj[:, i] = np.zeros([n_cells -num_new]) adj[i, i] = 1 adj = np.pad(adj, (0, num_new), 'constant') for r in new_idx: adj[i, r] = 1 adj[r, i] = 1 adj[r, r] = 1 if not conservative: orig = np.hstack([orig, orig[i]]) # adjacencies between pieces of isect and diff for r in new_idx: for k in new_idx: if r is k: continue if pc.is_adjacent(sol[r], sol[k]): adj[r, k] = 1 adj[k, r] = 1 msg = '' if logger.getEffectiveLevel() <= logging.DEBUG: msg += '\t\n Adding states ' + str(i) + ' and ' for r in new_idx: msg += str(r) + ' and ' msg += '\n' logger.debug(msg) for k in np.setdiff1d(old_adj, [i,n_cells-1]): # Every "old" neighbor must be the neighbor # of at least one of the new if pc.is_adjacent(sol[i], sol[k]): adj[i, k] = 1 adj[k, i] = 1 elif remove_trans and (trans_length == 1): # Actively remove transitions between non-neighbors transitions[i, k] = 0 transitions[k, i] = 0 for r in new_idx: if pc.is_adjacent(sol[r], sol[k]): adj[r, k] = 1 adj[k, r] = 1 elif remove_trans and (trans_length == 1): # Actively remove transitions between non-neighbors transitions[r, k] = 0 transitions[k, r] = 0 """Update IJ matrix""" IJ = np.pad(IJ, (0,num_new), 'constant') adj_k = reachable_within(trans_length, adj, adj) sym_adj_change(IJ, adj_k, transitions, i) for r in new_idx: sym_adj_change(IJ, adj_k, transitions, r) if logger.getEffectiveLevel() <= logging.DEBUG: msg = '\n\n Updated adj: \n' + str(adj) msg += '\n\n Updated trans: \n' + str(transitions) msg += '\n\n Updated IJ: \n' + str(IJ) logger.debug(msg) logger.info('Divided region: ' + str(i) + '\n') elif vol2 < abs_tol: logger.info('Found: ' + str(i) + ' ---> ' + str(j) + '\n') transitions[j,i] = 1 else: if logger.level <= logging.DEBUG: msg = '\t Unreachable: ' + str(i) + ' --X--> ' + str(j) + '\n' msg += '\t\t diff vol: ' + str(vol2) + '\n' msg += '\t\t intersect vol: ' + str(vol1) + '\n' logger.debug(msg) else: logger.info('\t unreachable\n') transitions[j,i] = 0 # check to avoid overlapping Regions if debug: tmp_part = PropPreservingPartition( domain=part.domain, regions=sol, adj=sp.lil_matrix(adj), prop_regions=part.prop_regions ) assert(tmp_part.is_partition() ) n_cells = len(sol) progress_ratio = 1 - float(np.sum(IJ) ) /n_cells**2 progress += [progress_ratio] msg = '\t total # polytopes: ' + str(n_cells) + '\n' msg += '\t progress ratio: ' + str(progress_ratio) + '\n' logger.info(msg) iter_count += 1 # no plotting ? if not plotit: continue if plt is None or plot_partition is None: continue if iter_count % plot_every != 0: continue tmp_part = PropPreservingPartition( domain=part.domain, regions=sol, adj=sp.lil_matrix(adj), prop_regions=part.prop_regions ) # plot pair under reachability check ax2.clear() si_tmp.plot(ax=ax2, color='green') sj_tmp.plot(ax2, color='red', hatch='o', alpha=0.5) plot_transition_arrow(si_tmp, sj_tmp, ax2) S0.plot(ax2, color='none', hatch='/', alpha=0.3) fig.canvas.draw() # plot partition ax1.clear() plot_partition(tmp_part, transitions.T, ax=ax1, color_seed=23) # plot dynamics ssys.plot(ax1, show_domain=False) # plot hatched continuous propositions part.plot_props(ax1) fig.canvas.draw() # scale view based on domain, # not only the current polytopes si, sj l,u = part.domain.bounding_box ax2.set_xlim(l[0,0], u[0,0]) ax2.set_ylim(l[1,0], u[1,0]) if save_img: fname = 'movie' +str(iter_count).zfill(3) fname += '.' + file_extension fig.savefig(fname, dpi=250) plt.pause(1) new_part = PropPreservingPartition( domain=part.domain, regions=sol, adj=sp.lil_matrix(adj), prop_regions=part.prop_regions ) # check completeness of adjacency matrix if debug: tmp_part = deepcopy(new_part) tmp_part.compute_adj() # Generate transition system and add transitions ofts = trs.FTS() adj = sp.lil_matrix(transitions.T) n = adj.shape[0] ofts_states = range(n) ofts.states.add_from(ofts_states) ofts.transitions.add_adj(adj, ofts_states) # Decorate TS with state labels atomic_propositions = set(part.prop_regions) ofts.atomic_propositions.add_from(atomic_propositions) for state, region in zip(ofts_states, sol): state_prop = region.props.copy() ofts.states.add(state, ap=state_prop) param = { 'N':N, 'trans_length':trans_length, 'closed_loop':closed_loop, 'conservative':conservative, 'use_all_horizon':use_all_horizon, 'min_cell_volume':min_cell_volume, 'max_num_poly':max_num_poly } ppp2orig = [part2orig[x] for x in orig] end_time = os.times()[0] msg = 'Total abstraction time: ' +\ str(end_time - start_time) + '[sec]' print(msg) logger.info(msg) if save_img and plt is not None: fig, ax = plt.subplots(1, 1) plt.plot(progress) ax.set_xlabel('iteration') ax.set_ylabel('progress ratio') ax.figure.savefig('progress.pdf') return AbstractPwa( ppp=new_part, ts=ofts, ppp2ts=ofts_states, pwa=ssys, pwa_ppp=part, ppp2pwa=orig, ppp2sys=subsys_list, orig_ppp=orig_ppp, ppp2orig=ppp2orig, disc_params=param )
def merge_partitions(abstractions): """Merge multiple abstractions. @param abstractions: keyed by mode @type abstractions: dict of L{AbstractPwa} @return: (merged_abstraction, ap_labeling) where: - merged_abstraction: L{AbstractSwitched} - ap_labeling: dict """ if len(abstractions) == 0: warnings.warn('Abstractions empty, nothing to merge.') return # consistency check for ab1 in abstractions.values(): for ab2 in abstractions.values(): p1 = ab1.ppp p2 = ab2.ppp if p1.prop_regions != p2.prop_regions: msg = 'merge: partitions have different sets ' msg += 'of continuous propositions' raise Exception(msg) if not (p1.domain.A == p2.domain.A).all() or \ not (p1.domain.b == p2.domain.b).all(): raise Exception('merge: partitions have different domains') # check equality of original PPP partitions if ab1.orig_ppp == ab2.orig_ppp: logger.info('original partitions happen to be equal') init_mode = list(abstractions.keys())[0] all_modes = set(abstractions) remaining_modes = all_modes.difference(set([init_mode])) print('init mode: ' + str(init_mode)) print('all modes: ' + str(all_modes)) print('remaining modes: ' + str(remaining_modes)) # initialize iteration data prev_modes = [init_mode] # Create a list of merged-together regions ab0 = abstractions[init_mode] regions = list(ab0.ppp) parents = {init_mode:list(range(len(regions) ))} ap_labeling = {i:reg.props for i,reg in enumerate(regions)} for cur_mode in remaining_modes: ab2 = abstractions[cur_mode] r = merge_partition_pair( regions, ab2, cur_mode, prev_modes, parents, ap_labeling ) regions, parents, ap_labeling = r prev_modes += [cur_mode] new_list = regions # build adjacency based on spatial adjacencies of # component abstractions. # which justifies the assumed symmetry of part1.adj, part2.adj # Basically, if two regions are either 1) part of the same region in one of # the abstractions or 2) adjacent in one of the abstractions, then the two # regions are adjacent in the switched dynamics. n_reg = len(new_list) adj = np.zeros([n_reg, n_reg], dtype=int) for i, reg_i in enumerate(new_list): for j, reg_j in enumerate(new_list[0:i]): touching = False for mode in abstractions: pi = parents[mode][i] pj = parents[mode][j] part = abstractions[mode].ppp if (part.adj[pi, pj] == 1) or (pi == pj): touching = True break if not touching: continue if pc.is_adjacent(reg_i, reg_j): adj[i,j] = 1 adj[j,i] = 1 adj[i,i] = 1 ppp = PropPreservingPartition( domain=ab0.ppp.domain, regions=new_list, prop_regions=ab0.ppp.prop_regions, adj=adj ) abstraction = AbstractSwitched( ppp=ppp, modes=abstractions, ppp2modes=parents, ) return (abstraction, ap_labeling)