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
22matrices for the simulation.
33
44You should implement your own metropolis-hastings or alternative algorithms
55as well as any steady-state or transition matrix optimization algorithms in
66this module.
77"""
88
9- from typing import Tuple , Any , Optional , List
9+ from typing import Tuple , Any , Optional
1010
1111import random
12- import cvxpy as cvx
1312import 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
2118from 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
2422OPTIMAL_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
229227def _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
294294def _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
314319def _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
336341def __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