Skip to content

Commit 4efda80

Browse files
Merge pull request #130 from FranciscoKloganB/develop
Publishing latest version of Hives simulator. All implementation is done. Some further improvements might be done later to plotting scripts.
2 parents 7a7a3a0 + 16331a3 commit 4efda80

48 files changed

Lines changed: 19825 additions & 966 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

hive/app/domain/cluster_groups.py

Lines changed: 798 additions & 495 deletions
Large diffs are not rendered by default.

hive/app/domain/helpers/exceptions.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,14 @@ def __init__(self, value=""):
2929
self.value = value
3030

3131
def __str__(self):
32-
return self.value
32+
return self.value
33+
34+
35+
class IllegalArgumentError(ValueError):
36+
"""Generic error used to indicate a parameter is not valid or expected.
37+
38+
Inherits:
39+
ValueError
40+
"""
41+
def __init__(self, value=""):
42+
super().__init__(value)

hive/app/domain/helpers/matrices.py

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
1-
"""Module used by :py:class:`~domain.cluster_groups.BaseHive to create transition
1+
"""Module used by :py:class:`~domain.cluster_groups.Cluster to create transition
22
matrices for the simulation.
33
44
You should implement your own metropolis-hastings or alternative algorithms
55
as well as any steady-state or transition matrix optimization algorithms in
66
this module.
77
"""
88

9-
from typing import Tuple, Any, Optional, List
9+
from typing import Tuple, Any, Optional
1010

1111
import random
12-
import cvxpy as cvx
1312
import numpy as np
14-
from cvxpy import SolverError, DCPError
15-
from matlab.engine import EngineError
13+
import cvxpy as cvx
1614

17-
from utils.randoms import random_index
15+
from matlab.engine import EngineError
16+
from scipy.sparse.csgraph import connected_components
1817

19-
from domain.helpers.exceptions import DistributionShapeError, MatrixError
20-
from domain.helpers.exceptions import MatrixNotSquareError
2118
from domain.helpers.matlab_utils import MatlabEngineContainer
22-
from scipy.sparse.csgraph import connected_components
19+
from domain.helpers.exceptions import *
20+
from utils.randoms import random_index
2321

2422
OPTIMAL_STATUS = {cvx.OPTIMAL, cvx.OPTIMAL_INACCURATE}
2523

@@ -79,7 +77,7 @@ def new_sdp_mh_transition_matrix(
7977
return t, get_mixing_rate(t)
8078
else:
8179
return None, float('inf')
82-
except (SolverError, DCPError):
80+
except (cvx.SolverError, cvx.DCPError):
8381
return None, float('inf')
8482

8583

@@ -133,7 +131,7 @@ def new_go_transition_matrix(
133131
return t.value.transpose(), get_mixing_rate(t.value)
134132
else:
135133
return None, float('inf')
136-
except (SolverError, DCPError):
134+
except (cvx.SolverError, cvx.DCPError):
137135
return None, float('inf')
138136

139137

@@ -228,8 +226,8 @@ def _adjency_matrix_sdp_optimization(
228226
# region Metropolis Hastings
229227
def _metropolis_hastings(a: np.ndarray,
230228
v_: np.ndarray,
231-
column_major_in: bool = False,
232-
column_major_out: bool = True) -> np.ndarray:
229+
column_major_out: bool = True,
230+
version: int = 2) -> np.ndarray:
233231
""" Constructs a transition matrix using metropolis-hastings algorithm.
234232
235233
Note:
@@ -242,12 +240,12 @@ def _metropolis_hastings(a: np.ndarray,
242240
v_:
243241
A stochastic vector that is the steady state of the resulting
244242
transition matrix.
245-
column_major_in:
246-
optional; Indicates whether adj_matrix given in input is in row
247-
or column major form.
248243
column_major_out:
249244
optional; Indicates whether to return transition_matrix output
250245
is in row or column major form.
246+
version:
247+
optional; Indicates which version of the algorith should be used
248+
(default is 2, for version 2).
251249
252250
Returns:
253251
An unlabeled transition matrix with steady state v_.
@@ -258,7 +256,7 @@ def _metropolis_hastings(a: np.ndarray,
258256
MatrixNotSquareError:
259257
When matrix a is not a square matrix.
260258
"""
261-
# Input checking
259+
262260
if v_.shape[0] != a.shape[1]:
263261
raise DistributionShapeError(
264262
"distribution shape: {}, proposal matrix shape: {}".format(
@@ -268,27 +266,29 @@ def _metropolis_hastings(a: np.ndarray,
268266
"rows: {}, columns: {}, expected square matrix".format(
269267
a.shape[0], a.shape[1]))
270268

271-
if column_major_in:
272-
a = a.transpose()
273-
274269
shape: Tuple[int, int] = a.shape
275270
size: int = a.shape[0]
276271

277272
rw: np.ndarray = _construct_random_walk_matrix(a)
278-
r: np.ndarray = _construct_rejection_matrix(rw, v_)
273+
if version == 1:
274+
rw = rw.transpose()
279275

280-
transition_matrix: np.ndarray = np.zeros(shape=shape)
276+
r: np.ndarray = _construct_rejection_matrix(rw, v_)
281277

278+
m: np.ndarray = np.zeros(shape=shape)
282279
for i in range(size):
283280
for j in range(size):
284281
if i != j:
285-
transition_matrix[i, j] = rw[i, j] * min(1, r[i, j])
286-
# after defining all p[i, j] we can safely defined p[i, i], i.e.: define p[i, j] when i = j
287-
transition_matrix[i, i] = __get_diagonal_entry_probability(rw, r, i)
282+
m[i, j] = rw[i, j] * min(1, r[i, j])
283+
if version == 1:
284+
m[i, i] = __get_diagonal_entry_probability(rw, r, i)
285+
elif version == 2:
286+
m[i, i] = __get_diagonal_entry_probability_v2(m, i)
288287

289288
if column_major_out:
290-
return transition_matrix.transpose()
291-
return transition_matrix
289+
return m.transpose()
290+
291+
return m
292292

293293

294294
def _construct_random_walk_matrix(a: np.ndarray) -> np.ndarray:
@@ -301,14 +301,19 @@ def _construct_random_walk_matrix(a: np.ndarray) -> np.ndarray:
301301
Returns:
302302
A matrix representing the performed random walk.
303303
"""
304-
shape = a.shape
305-
size = shape[0]
306-
rw: np.ndarray = np.zeros(shape=shape)
307-
for i in range(size):
308-
degree: Any = np.sum(a[i, :]) # all possible states reachable from state i, including self
309-
for j in range(size):
310-
rw[i, j] = a[i, j] / degree
311-
return rw
304+
# Version 1.
305+
# shape = a.shape
306+
# size = shape[0]
307+
# rw: np.ndarray = np.zeros(shape=shape)
308+
# for i in range(size):
309+
# # all possible states reachable from state i, including self
310+
# degree: Any = np.sum(a[i, :])
311+
# for j in range(size):
312+
# rw[i, j] = a[i, j] / degree
313+
# return rw
314+
# Version 2 - Returns Column Major Random Walk, similar to MatLab.
315+
# To return a equivalent of version 1 output, transpose the result.
316+
return a / np.sum(a, axis=1)
312317

313318

314319
def _construct_rejection_matrix(rw: np.ndarray, v_: np.array) -> np.ndarray:
@@ -334,7 +339,7 @@ def _construct_rejection_matrix(rw: np.ndarray, v_: np.array) -> np.ndarray:
334339

335340

336341
def __get_diagonal_entry_probability(
337-
rw: np.ndarray, r: np.ndarray, i: int) -> np.int32:
342+
rw: np.ndarray, r: np.ndarray, i: int) -> np.float64:
338343
"""Helper function used by _metropolis_hastings function.
339344
340345
Calculates the value that should be assigned to the entry (i, i) of the
@@ -356,10 +361,31 @@ def __get_diagonal_entry_probability(
356361
outputed by the _metropolis_hastings function.
357362
"""
358363
size: int = rw.shape[0]
359-
pii: np.int32 = rw[i, i]
364+
pii: np.float64 = rw[i, i]
360365
for k in range(size):
361366
pii += rw[i, k] * (1 - min(1, r[i, k]))
362367
return pii
368+
369+
370+
def __get_diagonal_entry_probability_v2(m: np.ndarray, i: int) -> np.float64:
371+
"""Helper function used by _metropolis_hastings function.
372+
373+
Calculates the value that should be assigned to the entry (i, i) of the
374+
transition matrix being calculated by the metropolis hastings algorithm
375+
by considering the rejection probability over the random walk that was
376+
performed on an adjacency matrix.
377+
378+
Args:
379+
m:
380+
The matrix to receive the diagonal entry value.
381+
i:
382+
The diagonal entry index. E.g.: m[i, i].
383+
384+
Returns:
385+
A probability to be inserted at entry (i, i) of the transition matrix
386+
outputed by the _metropolis_hastings function.
387+
"""
388+
return 1 - np.sum(m[i, :])
363389
# endregion
364390

365391

@@ -383,13 +409,15 @@ def get_mixing_rate(m: np.ndarray) -> float:
383409
if size != m.shape[1]:
384410
raise MatrixNotSquareError(
385411
"Can not compute eigenvalues/vectors with non-square matrix")
386-
387-
eigenvalues, eigenvectors = np.linalg.eig(m - np.ones((size, size)) / size)
412+
m = m - (np.ones((size, size)) / size)
413+
eigenvalues, eigenvectors = np.linalg.eig(m)
388414
mixing_rate = np.max(np.abs(eigenvalues))
389415
return mixing_rate.item()
390416

391417

392-
def new_symmetric_matrix(size: int) -> np.ndarray:
418+
def new_symmetric_matrix(
419+
size: int, allow_sloops: bool = True, force_sloops: bool = True
420+
) -> np.ndarray:
393421
"""Generates a random symmetric matrix.
394422
395423
The generated adjacency matrix does not have transient state sets or
@@ -399,22 +427,50 @@ def new_symmetric_matrix(size: int) -> np.ndarray:
399427
Args:
400428
size:
401429
The length of the square matrix.
430+
allow_sloops:
431+
Indicates if the generated adjacency matrix allows diagonal
432+
entries representing self-loops. If false, then, all diagonal
433+
entries must be zeros. Otherwise, they can be zeros or ones (
434+
default is True).
435+
force_sloops:
436+
Indicates if the diagonal of the generated matrix should be
437+
filled with ones. If false, valid diagonal entries are decided by
438+
`allow_self_loops` param. Otherwise, diagonal entries are filled
439+
with ones. If `allow_self_loops` is False and `enforce_loops` is
440+
True, an error is raised (default is True).
402441
403442
Returns:
404443
The adjency matrix representing the connections between a
405444
groups of network nodes.
445+
446+
Raises:
447+
IllegalArgumentError:
448+
When `allow_self_loops` (False) conflicts with
449+
`enforce_loops` (True).
406450
"""
451+
if not allow_sloops and force_sloops:
452+
raise IllegalArgumentError("Can not invoke new_symmetric_matrix with:\n"
453+
" [x] allow_sloops=False\n"
454+
" [x] force_sloops=True")
407455
secure_random = random.SystemRandom()
408456
m = np.zeros((size, size))
409457
for i in range(size):
410458
for j in range(i, size):
411-
p = secure_random.uniform(0.0, 1.0)
412-
edge_val = np.ceil(p) if p >= 0.5 else np.floor(p)
413-
m[i, j] = m[j, i] = edge_val
459+
if i == j:
460+
if not allow_sloops:
461+
m[i, i] = 0
462+
elif force_sloops:
463+
m[i, i] = 1
464+
else:
465+
m[i, i] = __new_edge_val__(secure_random)
466+
else:
467+
m[i, j] = m[j, i] = __new_edge_val__(secure_random)
414468
return m
415469

416470

417-
def new_symmetric_connected_matrix(size: int) -> np.ndarray:
471+
def new_symmetric_connected_matrix(
472+
size: int, allow_sloops: bool = True, force_sloops: bool = True
473+
) -> np.ndarray:
418474
"""Generates a random symmetric matrix which is also connected.
419475
420476
See :py:func:`~domain.helpers.matrices.new_symmetric_matrix` and
@@ -423,6 +479,10 @@ def new_symmetric_connected_matrix(size: int) -> np.ndarray:
423479
Args:
424480
size:
425481
The length of the square matrix.
482+
allow_sloops:
483+
See :py:func:`~domain.helpers.matrices.new_symmetric_matrix`.
484+
force_sloops:
485+
See :py:func:`~domain.helpers.matrices.new_symmetric_matrix`.
426486
427487
Returns:
428488
A matrix that represents an adjacency matrix that is also connected.
@@ -492,4 +552,9 @@ def is_connected(m: np.ndarray, directed: bool = False) -> bool:
492552
"""
493553
n, cc_labels = connected_components(m, directed=directed)
494554
return n == 1
555+
556+
557+
def __new_edge_val__(random_generator: random.SystemRandom) -> np.float64:
558+
p = random_generator.uniform(0.0, 1.0)
559+
return np.ceil(p) if p >= 0.5 else np.floor(p)
495560
# endregion

0 commit comments

Comments
 (0)