triangulation.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. import numpy as np
  2. import copy
  3. class Complex:
  4. def __init__(self, dim, func, func_args=(), symmetry=False, bounds=None,
  5. g_cons=None, g_args=()):
  6. self.dim = dim
  7. self.bounds = bounds
  8. self.symmetry = symmetry # TODO: Define the functions to be used
  9. # here in init to avoid if checks
  10. self.gen = 0
  11. self.perm_cycle = 0
  12. # Every cell is stored in a list of its generation,
  13. # e.g., the initial cell is stored in self.H[0]
  14. # 1st get new cells are stored in self.H[1] etc.
  15. # When a cell is subgenerated it is removed from this list
  16. self.H = [] # Storage structure of cells
  17. # Cache of all vertices
  18. self.V = VertexCache(func, func_args, bounds, g_cons, g_args)
  19. # Generate n-cube here:
  20. self.n_cube(dim, symmetry=symmetry)
  21. # TODO: Assign functions to a the complex instead
  22. if symmetry:
  23. self.generation_cycle = 1
  24. # self.centroid = self.C0()[-1].x
  25. # self.C0.centroid = self.centroid
  26. else:
  27. self.add_centroid()
  28. self.H.append([])
  29. self.H[0].append(self.C0)
  30. self.hgr = self.C0.homology_group_rank()
  31. self.hgrd = 0 # Complex group rank differential
  32. # self.hgr = self.C0.hg_n
  33. # Build initial graph
  34. self.graph_map()
  35. self.performance = []
  36. self.performance.append(0)
  37. self.performance.append(0)
  38. def __call__(self):
  39. return self.H
  40. def n_cube(self, dim, symmetry=False, printout=False):
  41. """
  42. Generate the simplicial triangulation of the N-D hypercube
  43. containing 2**n vertices
  44. """
  45. origin = list(np.zeros(dim, dtype=int))
  46. self.origin = origin
  47. supremum = list(np.ones(dim, dtype=int))
  48. self.supremum = supremum
  49. # tuple versions for indexing
  50. origintuple = tuple(origin)
  51. supremumtuple = tuple(supremum)
  52. x_parents = [origintuple]
  53. if symmetry:
  54. self.C0 = Simplex(0, 0, 0, self.dim) # Initial cell object
  55. self.C0.add_vertex(self.V[origintuple])
  56. i_s = 0
  57. self.perm_symmetry(i_s, x_parents, origin)
  58. self.C0.add_vertex(self.V[supremumtuple])
  59. else:
  60. self.C0 = Cell(0, 0, origin, supremum) # Initial cell object
  61. self.C0.add_vertex(self.V[origintuple])
  62. self.C0.add_vertex(self.V[supremumtuple])
  63. i_parents = []
  64. self.perm(i_parents, x_parents, origin)
  65. if printout:
  66. print("Initial hyper cube:")
  67. for v in self.C0():
  68. v.print_out()
  69. def perm(self, i_parents, x_parents, xi):
  70. # TODO: Cut out of for if outside linear constraint cutting planes
  71. xi_t = tuple(xi)
  72. # Construct required iterator
  73. iter_range = [x for x in range(self.dim) if x not in i_parents]
  74. for i in iter_range:
  75. i2_parents = copy.copy(i_parents)
  76. i2_parents.append(i)
  77. xi2 = copy.copy(xi)
  78. xi2[i] = 1
  79. # Make new vertex list a hashable tuple
  80. xi2_t = tuple(xi2)
  81. # Append to cell
  82. self.C0.add_vertex(self.V[xi2_t])
  83. # Connect neighbors and vice versa
  84. # Parent point
  85. self.V[xi2_t].connect(self.V[xi_t])
  86. # Connect all family of simplices in parent containers
  87. for x_ip in x_parents:
  88. self.V[xi2_t].connect(self.V[x_ip])
  89. x_parents2 = copy.copy(x_parents)
  90. x_parents2.append(xi_t)
  91. # Permutate
  92. self.perm(i2_parents, x_parents2, xi2)
  93. def perm_symmetry(self, i_s, x_parents, xi):
  94. # TODO: Cut out of for if outside linear constraint cutting planes
  95. xi_t = tuple(xi)
  96. xi2 = copy.copy(xi)
  97. xi2[i_s] = 1
  98. # Make new vertex list a hashable tuple
  99. xi2_t = tuple(xi2)
  100. # Append to cell
  101. self.C0.add_vertex(self.V[xi2_t])
  102. # Connect neighbors and vice versa
  103. # Parent point
  104. self.V[xi2_t].connect(self.V[xi_t])
  105. # Connect all family of simplices in parent containers
  106. for x_ip in x_parents:
  107. self.V[xi2_t].connect(self.V[x_ip])
  108. x_parents2 = copy.copy(x_parents)
  109. x_parents2.append(xi_t)
  110. i_s += 1
  111. if i_s == self.dim:
  112. return
  113. # Permutate
  114. self.perm_symmetry(i_s, x_parents2, xi2)
  115. def add_centroid(self):
  116. """Split the central edge between the origin and supremum of
  117. a cell and add the new vertex to the complex"""
  118. self.centroid = list(
  119. (np.array(self.origin) + np.array(self.supremum)) / 2.0)
  120. self.C0.add_vertex(self.V[tuple(self.centroid)])
  121. self.C0.centroid = self.centroid
  122. # Disconnect origin and supremum
  123. self.V[tuple(self.origin)].disconnect(self.V[tuple(self.supremum)])
  124. # Connect centroid to all other vertices
  125. for v in self.C0():
  126. self.V[tuple(self.centroid)].connect(self.V[tuple(v.x)])
  127. self.centroid_added = True
  128. return
  129. # Construct incidence array:
  130. def incidence(self):
  131. if self.centroid_added:
  132. self.structure = np.zeros([2 ** self.dim + 1, 2 ** self.dim + 1],
  133. dtype=int)
  134. else:
  135. self.structure = np.zeros([2 ** self.dim, 2 ** self.dim],
  136. dtype=int)
  137. for v in self.HC.C0():
  138. for v2 in v.nn:
  139. self.structure[v.index, v2.index] = 1
  140. return
  141. # A more sparse incidence generator:
  142. def graph_map(self):
  143. """ Make a list of size 2**n + 1 where an entry is a vertex
  144. incidence, each list element contains a list of indexes
  145. corresponding to that entries neighbors"""
  146. self.graph = [[v2.index for v2 in v.nn] for v in self.C0()]
  147. # Graph structure method:
  148. # 0. Capture the indices of the initial cell.
  149. # 1. Generate new origin and supremum scalars based on current generation
  150. # 2. Generate a new set of vertices corresponding to a new
  151. # "origin" and "supremum"
  152. # 3. Connected based on the indices of the previous graph structure
  153. # 4. Disconnect the edges in the original cell
  154. def sub_generate_cell(self, C_i, gen):
  155. """Subgenerate a cell `C_i` of generation `gen` and
  156. homology group rank `hgr`."""
  157. origin_new = tuple(C_i.centroid)
  158. centroid_index = len(C_i()) - 1
  159. # If not gen append
  160. try:
  161. self.H[gen]
  162. except IndexError:
  163. self.H.append([])
  164. # Generate subcubes using every extreme vertex in C_i as a supremum
  165. # and the centroid of C_i as the origin
  166. H_new = [] # list storing all the new cubes split from C_i
  167. for i, v in enumerate(C_i()[:-1]):
  168. supremum = tuple(v.x)
  169. H_new.append(
  170. self.construct_hypercube(origin_new, supremum, gen, C_i.hg_n))
  171. for i, connections in enumerate(self.graph):
  172. # Present vertex V_new[i]; connect to all connections:
  173. if i == centroid_index: # Break out of centroid
  174. break
  175. for j in connections:
  176. C_i()[i].disconnect(C_i()[j])
  177. # Destroy the old cell
  178. if C_i is not self.C0: # Garbage collector does this anyway; not needed
  179. del C_i
  180. # TODO: Recalculate all the homology group ranks of each cell
  181. return H_new
  182. def split_generation(self):
  183. """
  184. Run sub_generate_cell for every cell in the current complex self.gen
  185. """
  186. no_splits = False # USED IN SHGO
  187. try:
  188. for c in self.H[self.gen]:
  189. if self.symmetry:
  190. # self.sub_generate_cell_symmetry(c, self.gen + 1)
  191. self.split_simplex_symmetry(c, self.gen + 1)
  192. else:
  193. self.sub_generate_cell(c, self.gen + 1)
  194. except IndexError:
  195. no_splits = True # USED IN SHGO
  196. self.gen += 1
  197. return no_splits # USED IN SHGO
  198. def construct_hypercube(self, origin, supremum, gen, hgr,
  199. printout=False):
  200. """
  201. Build a hypercube with triangulations symmetric to C0.
  202. Parameters
  203. ----------
  204. origin : vec
  205. supremum : vec (tuple)
  206. gen : generation
  207. hgr : parent homology group rank
  208. """
  209. # Initiate new cell
  210. v_o = np.array(origin)
  211. v_s = np.array(supremum)
  212. C_new = Cell(gen, hgr, origin, supremum)
  213. C_new.centroid = tuple((v_o + v_s) * .5)
  214. # Build new indexed vertex list
  215. V_new = []
  216. for i, v in enumerate(self.C0()[:-1]):
  217. v_x = np.array(v.x)
  218. sub_cell_t1 = v_o - v_o * v_x
  219. sub_cell_t2 = v_s * v_x
  220. vec = sub_cell_t1 + sub_cell_t2
  221. vec = tuple(vec)
  222. C_new.add_vertex(self.V[vec])
  223. V_new.append(vec)
  224. # Add new centroid
  225. C_new.add_vertex(self.V[C_new.centroid])
  226. V_new.append(C_new.centroid)
  227. # Connect new vertices #TODO: Thread into other loop; no need for V_new
  228. for i, connections in enumerate(self.graph):
  229. # Present vertex V_new[i]; connect to all connections:
  230. for j in connections:
  231. self.V[V_new[i]].connect(self.V[V_new[j]])
  232. if printout:
  233. print("A sub hyper cube with:")
  234. print("origin: {}".format(origin))
  235. print("supremum: {}".format(supremum))
  236. for v in C_new():
  237. v.print_out()
  238. # Append the new cell to the to complex
  239. self.H[gen].append(C_new)
  240. return C_new
  241. def split_simplex_symmetry(self, S, gen):
  242. """
  243. Split a hypersimplex S into two sub simplices by building a hyperplane
  244. which connects to a new vertex on an edge (the longest edge in
  245. dim = {2, 3}) and every other vertex in the simplex that is not
  246. connected to the edge being split.
  247. This function utilizes the knowledge that the problem is specified
  248. with symmetric constraints
  249. The longest edge is tracked by an ordering of the
  250. vertices in every simplices, the edge between first and second
  251. vertex is the longest edge to be split in the next iteration.
  252. """
  253. # If not gen append
  254. try:
  255. self.H[gen]
  256. except IndexError:
  257. self.H.append([])
  258. # Find new vertex.
  259. # V_new_x = tuple((np.array(C()[0].x) + np.array(C()[1].x)) / 2.0)
  260. s = S()
  261. firstx = s[0].x
  262. lastx = s[-1].x
  263. V_new = self.V[tuple((np.array(firstx) + np.array(lastx)) / 2.0)]
  264. # Disconnect old longest edge
  265. self.V[firstx].disconnect(self.V[lastx])
  266. # Connect new vertices to all other vertices
  267. for v in s[:]:
  268. v.connect(self.V[V_new.x])
  269. # New "lower" simplex
  270. S_new_l = Simplex(gen, S.hg_n, self.generation_cycle,
  271. self.dim)
  272. S_new_l.add_vertex(s[0])
  273. S_new_l.add_vertex(V_new) # Add new vertex
  274. for v in s[1:-1]: # Add all other vertices
  275. S_new_l.add_vertex(v)
  276. # New "upper" simplex
  277. S_new_u = Simplex(gen, S.hg_n, S.generation_cycle, self.dim)
  278. # First vertex on new long edge
  279. S_new_u.add_vertex(s[S_new_u.generation_cycle + 1])
  280. for v in s[1:-1]: # Remaining vertices
  281. S_new_u.add_vertex(v)
  282. for k, v in enumerate(s[1:-1]): # iterate through inner vertices
  283. if k == S.generation_cycle:
  284. S_new_u.add_vertex(V_new)
  285. else:
  286. S_new_u.add_vertex(v)
  287. S_new_u.add_vertex(s[-1]) # Second vertex on new long edge
  288. self.H[gen].append(S_new_l)
  289. self.H[gen].append(S_new_u)
  290. return
  291. # Plots
  292. def plot_complex(self):
  293. """
  294. Here, C is the LIST of simplexes S in the
  295. 2- or 3-D complex
  296. To plot a single simplex S in a set C, use e.g., [C[0]]
  297. """
  298. from matplotlib import pyplot
  299. if self.dim == 2:
  300. pyplot.figure()
  301. for C in self.H:
  302. for c in C:
  303. for v in c():
  304. if self.bounds is None:
  305. x_a = np.array(v.x, dtype=float)
  306. else:
  307. x_a = np.array(v.x, dtype=float)
  308. for i in range(len(self.bounds)):
  309. x_a[i] = (x_a[i] * (self.bounds[i][1]
  310. - self.bounds[i][0])
  311. + self.bounds[i][0])
  312. # logging.info('v.x_a = {}'.format(x_a))
  313. pyplot.plot([x_a[0]], [x_a[1]], 'o')
  314. xlines = []
  315. ylines = []
  316. for vn in v.nn:
  317. if self.bounds is None:
  318. xn_a = np.array(vn.x, dtype=float)
  319. else:
  320. xn_a = np.array(vn.x, dtype=float)
  321. for i in range(len(self.bounds)):
  322. xn_a[i] = (xn_a[i] * (self.bounds[i][1]
  323. - self.bounds[i][0])
  324. + self.bounds[i][0])
  325. # logging.info('vn.x = {}'.format(vn.x))
  326. xlines.append(xn_a[0])
  327. ylines.append(xn_a[1])
  328. xlines.append(x_a[0])
  329. ylines.append(x_a[1])
  330. pyplot.plot(xlines, ylines)
  331. if self.bounds is None:
  332. pyplot.ylim([-1e-2, 1 + 1e-2])
  333. pyplot.xlim([-1e-2, 1 + 1e-2])
  334. else:
  335. pyplot.ylim(
  336. [self.bounds[1][0] - 1e-2, self.bounds[1][1] + 1e-2])
  337. pyplot.xlim(
  338. [self.bounds[0][0] - 1e-2, self.bounds[0][1] + 1e-2])
  339. pyplot.show()
  340. elif self.dim == 3:
  341. fig = pyplot.figure()
  342. ax = fig.add_subplot(111, projection='3d')
  343. for C in self.H:
  344. for c in C:
  345. for v in c():
  346. x = []
  347. y = []
  348. z = []
  349. # logging.info('v.x = {}'.format(v.x))
  350. x.append(v.x[0])
  351. y.append(v.x[1])
  352. z.append(v.x[2])
  353. for vn in v.nn:
  354. x.append(vn.x[0])
  355. y.append(vn.x[1])
  356. z.append(vn.x[2])
  357. x.append(v.x[0])
  358. y.append(v.x[1])
  359. z.append(v.x[2])
  360. # logging.info('vn.x = {}'.format(vn.x))
  361. ax.plot(x, y, z, label='simplex')
  362. pyplot.show()
  363. else:
  364. print("dimension higher than 3 or wrong complex format")
  365. return
  366. class VertexGroup:
  367. def __init__(self, p_gen, p_hgr):
  368. self.p_gen = p_gen # parent generation
  369. self.p_hgr = p_hgr # parent homology group rank
  370. self.hg_n = None
  371. self.hg_d = None
  372. # Maybe add parent homology group rank total history
  373. # This is the sum off all previously split cells
  374. # cumulatively throughout its entire history
  375. self.C = []
  376. def __call__(self):
  377. return self.C
  378. def add_vertex(self, V):
  379. if V not in self.C:
  380. self.C.append(V)
  381. def homology_group_rank(self):
  382. """
  383. Returns the homology group order of the current cell
  384. """
  385. if self.hg_n is None:
  386. self.hg_n = sum(1 for v in self.C if v.minimiser())
  387. return self.hg_n
  388. def homology_group_differential(self):
  389. """
  390. Returns the difference between the current homology group of the
  391. cell and its parent group
  392. """
  393. if self.hg_d is None:
  394. self.hgd = self.hg_n - self.p_hgr
  395. return self.hgd
  396. def polytopial_sperner_lemma(self):
  397. """
  398. Returns the number of stationary points theoretically contained in the
  399. cell based information currently known about the cell
  400. """
  401. pass
  402. def print_out(self):
  403. """
  404. Print the current cell to console
  405. """
  406. for v in self():
  407. v.print_out()
  408. class Cell(VertexGroup):
  409. """
  410. Contains a cell that is symmetric to the initial hypercube triangulation
  411. """
  412. def __init__(self, p_gen, p_hgr, origin, supremum):
  413. super().__init__(p_gen, p_hgr)
  414. self.origin = origin
  415. self.supremum = supremum
  416. self.centroid = None # (Not always used)
  417. # TODO: self.bounds
  418. class Simplex(VertexGroup):
  419. """
  420. Contains a simplex that is symmetric to the initial symmetry constrained
  421. hypersimplex triangulation
  422. """
  423. def __init__(self, p_gen, p_hgr, generation_cycle, dim):
  424. super().__init__(p_gen, p_hgr)
  425. self.generation_cycle = (generation_cycle + 1) % (dim - 1)
  426. class Vertex:
  427. def __init__(self, x, bounds=None, func=None, func_args=(), g_cons=None,
  428. g_cons_args=(), nn=None, index=None):
  429. self.x = x
  430. self.order = sum(x)
  431. x_a = np.array(x, dtype=float)
  432. if bounds is not None:
  433. for i, (lb, ub) in enumerate(bounds):
  434. x_a[i] = x_a[i] * (ub - lb) + lb
  435. # TODO: Make saving the array structure optional
  436. self.x_a = x_a
  437. # Note Vertex is only initiated once for all x so only
  438. # evaluated once
  439. if func is not None:
  440. self.feasible = True
  441. if g_cons is not None:
  442. for g, args in zip(g_cons, g_cons_args):
  443. if g(self.x_a, *args) < 0.0:
  444. self.f = np.inf
  445. self.feasible = False
  446. break
  447. if self.feasible:
  448. self.f = func(x_a, *func_args)
  449. if nn is not None:
  450. self.nn = nn
  451. else:
  452. self.nn = set()
  453. self.fval = None
  454. self.check_min = True
  455. # Index:
  456. if index is not None:
  457. self.index = index
  458. def __hash__(self):
  459. return hash(self.x)
  460. def connect(self, v):
  461. if v is not self and v not in self.nn:
  462. self.nn.add(v)
  463. v.nn.add(self)
  464. if self.minimiser():
  465. v._min = False
  466. v.check_min = False
  467. # TEMPORARY
  468. self.check_min = True
  469. v.check_min = True
  470. def disconnect(self, v):
  471. if v in self.nn:
  472. self.nn.remove(v)
  473. v.nn.remove(self)
  474. self.check_min = True
  475. v.check_min = True
  476. def minimiser(self):
  477. """Check whether this vertex is strictly less than all its neighbors"""
  478. if self.check_min:
  479. self._min = all(self.f < v.f for v in self.nn)
  480. self.check_min = False
  481. return self._min
  482. def print_out(self):
  483. print("Vertex: {}".format(self.x))
  484. constr = 'Connections: '
  485. for vc in self.nn:
  486. constr += '{} '.format(vc.x)
  487. print(constr)
  488. print('Order = {}'.format(self.order))
  489. class VertexCache:
  490. def __init__(self, func, func_args=(), bounds=None, g_cons=None,
  491. g_cons_args=(), indexed=True):
  492. self.cache = {}
  493. self.func = func
  494. self.g_cons = g_cons
  495. self.g_cons_args = g_cons_args
  496. self.func_args = func_args
  497. self.bounds = bounds
  498. self.nfev = 0
  499. self.size = 0
  500. if indexed:
  501. self.index = -1
  502. def __getitem__(self, x, indexed=True):
  503. try:
  504. return self.cache[x]
  505. except KeyError:
  506. if indexed:
  507. self.index += 1
  508. xval = Vertex(x, bounds=self.bounds,
  509. func=self.func, func_args=self.func_args,
  510. g_cons=self.g_cons,
  511. g_cons_args=self.g_cons_args,
  512. index=self.index)
  513. else:
  514. xval = Vertex(x, bounds=self.bounds,
  515. func=self.func, func_args=self.func_args,
  516. g_cons=self.g_cons,
  517. g_cons_args=self.g_cons_args)
  518. # logging.info("New generated vertex at x = {}".format(x))
  519. # NOTE: Surprisingly high performance increase if logging is commented out
  520. self.cache[x] = xval
  521. # TODO: Check
  522. if self.func is not None:
  523. if self.g_cons is not None:
  524. if xval.feasible:
  525. self.nfev += 1
  526. self.size += 1
  527. else:
  528. self.size += 1
  529. else:
  530. self.nfev += 1
  531. self.size += 1
  532. return self.cache[x]