11import numpy as np
22import matplotlib .pyplot as plt
3+ import pathlib
34
45
56class ChaosGame :
67 def __init__ (self , n : int = 3 , r : float = 0.5 ):
78 """Need to make sure input is of right type, and 0 < r < 1, n > 2.
8- This checks first, then sets the attributes after. sets to default if not possible. """
9+ This checks first, then sets the attributes after. sets to default if not possible.
10+ _corners are 2d arrays of floats, so are _points. """
11+ self ._n : int
12+ self ._r : float
13+ self ._solved = False
14+ # not sure how to type corners and points,
15+ # which are lists of tuples then converted to np arrays
16+ self ._corners : list
17+ self ._points = np .asarray ([(0. , 0. , 0 )])
18+
19+ # better way to handle all situations? this looks a bit ugly to me in constructor
920 try :
1021 if not isinstance (n , int ):
1122 raise TypeError ("n should be an int" )
@@ -15,25 +26,132 @@ def __init__(self, n: int = 3, r: float = 0.5):
1526 n = int (n )
1627 if n < - 2 :
1728 print ("n need to be int > 2, converting to positive int" )
18- self .n = self .n * - 1
29+ self ._n = self ._n * - 1
1930 elif - 2 <= n <= 2 :
31+ print ("n has to be int > 2" )
2032 print ("using default n = 3" )
21- self .n = 3
33+ self ._n = 3
2234 else :
23- self .n = n
35+ self ._n = n
2436
2537 try :
2638 if not isinstance (r , float ):
27- raise TypeError ("r should be a float 0 < r < 1" )
39+ raise TypeError ("TypeError: r should be a float 0 < r < 1" )
2840 except TypeError as terr :
2941 print (terr .args )
3042 print ("trying to convert" )
3143 r = float (r )
3244 if not (0 < r < 1 ):
33- print ("using default = 0.5" )
34- self .r = 0.5
45+ print ("r should be a float 0 < r < 1" )
46+ print ("using default r = 0.5" )
47+ self ._r = 0.5
48+ else :
49+ self ._r = r
50+
51+ self ._generate_ngon ()
52+
53+ def _generate_ngon (self ):
54+ """Generates and saves the corner points of the ngon.
55+ saved as 2d array of floats"""
56+ theta = 2 * np .pi / self ._n
57+ corners = [(np .sin (theta ), np .cos (theta ))] * self ._n
58+ for i in range (self ._n ):
59+ corners [i ] = (np .sin (theta * i ), np .cos (theta * i ))
60+ self ._corners = np .asarray (corners )
61+
62+ def plot_ngon (self ):
63+ plt .plot (self ._corners [:, 0 ], self ._corners [:, 1 ])
64+
65+ def _starting_point (self ):
66+ """Randomly selects a starting point inside the ngon
67+ :return: python list with 2 elements of float type
68+ """
69+ spoint = [0 , 0 ]
70+ w = [0. ] * self ._n
71+ for i in range (self ._n ):
72+ w [i ] = np .random .random ()
73+ wsum = sum (w )
74+ for i in range (self ._n ):
75+ w [i ] = w [i ] / wsum
76+ spoint [0 ] += w [i ] * self ._corners [i ][0 ]
77+ spoint [1 ] += w [i ] * self ._corners [i ][1 ]
78+ return spoint
79+
80+ def iterate (self , steps : int = 10 , discard : int = 5 ):
81+ """Discards the first discard points.
82+ The third element in each tuple is the random chosen corner index
83+ be aware that, currently, index is a float"""
84+ current_point = np .asarray (self ._starting_point ())
85+
86+ self ._points = [(0. , 0. , 0 )] * (steps - discard )
87+ self ._points = np .asarray (self ._points )
88+ for i in range (discard ):
89+ c = self ._corners [np .random .randint (0 , self ._n )]
90+ current_point = self ._r * current_point + (1 - self ._r ) * c
91+
92+ for i in range (steps - discard ):
93+ cind = np .random .randint (0 , self ._n )
94+ current_point = self ._r * current_point + (1 - self ._r ) * self ._corners [cind ]
95+ self ._points [i ] = * current_point , cind
96+ self ._solved = True
97+
98+ def plot (self , color : bool , cmap : str ):
99+ """Colors is a tuple of the corner indices, when color=True. """
100+ colors = "black"
101+ if color :
102+ colors = self .gradient_color
103+ if self ._solved :
104+ plt .scatter (self ._points [:, 0 ], self ._points [:, 1 ], c = colors ,
105+ cmap = cmap , s = 0.1 )
106+ else :
107+ raise Exception ("Need to iterate() before plotting" )
108+
109+ def show (self , color = False , cmap = "rainbow" ):
110+ plt .axis ("Equal" )
111+ plt .axis ('off' )
112+ self .plot (color , cmap )
113+ plt .show ()
114+
115+ def savepng (self , outfile : str , color = False , cmap = "rainbow" ):
116+ plt .axis ("Equal" )
117+ plt .axis ('off' )
118+ self .plot (color , cmap )
119+ plt .savefig (pathlib .Path (__file__ ).parent .resolve ().__str__ () + '\\ figures\\ ' + outfile ,
120+ dpi = 400 )
121+
122+ @property
123+ def gradient_color (self ):
124+ """Returns an array with each points rgb colors calculated based on
125+ the selected corner and the previous points color
126+ gc = gradiant colors, cc = corner colors
127+
128+ :return: array of shape (x, 3). x depends on how many iterated points
129+ """
130+ if self ._solved :
131+ colors = iter ([plt .cm .tab20 (i ) for i in range (20 )])
132+ cc = []
133+ i = 0
134+ # TODO: this can only create a limited number of colors then repeat
135+ while len (cc ) < self ._corners .shape [0 ]:
136+ cc .append (next (colors ))
137+ i += 1
138+ if i == 20 :
139+ colors = iter ([plt .cm .tab20b (i ) for i in range (20 )])
140+ i = 0
141+ cc = np .delete (np .asarray (cc ), 3 , 1 )
142+ # probably exists better way to initialize gc than converting back to list
143+ gc = np .asarray ([cc [int (self ._points [0 ][2 ])]] * self ._points .shape [0 ])
144+ # i need an arbitrary amount of chosen colors for the corners
145+ for i in range (1 , self ._points .shape [0 ]):
146+ gc [i ] = (gc [i - 1 ] + cc [int (self ._points [i ][2 ])]) / 2
147+ return gc
35148 else :
36- self .r = r
149+ raise Exception ("Need to iterate() before creating colors" )
150+
151+ # testing stuff
152+ # game = ChaosGame(6, 1/3)
37153
154+ # game.iterate(20000, 100)
38155
39- game = ChaosGame ()
156+ # game.show(True)
157+ # game.savepng("stonks", True)
0 commit comments