6
6
import scipy .interpolate
7
7
import scipy .optimize
8
8
9
- from classy_blocks .grading .autograding .params .base import sum_length
9
+ from classy_blocks .grading .autograding .params .approximator import Approximator
10
10
from classy_blocks .grading .autograding .params .layers import InflationLayer
11
11
from classy_blocks .grading .chop import Chop
12
12
from classy_blocks .types import FloatListType
17
17
class DistributorBase (abc .ABC ):
18
18
"""Algorithm that creates chops from given count and sizes;
19
19
first distributes cells using a predefined 'ideal' sizes and ratios,
20
- then optimizes their individual size to get as close to that ideal as possible.
20
+ then optimizes their * individual* size to get as close to that ideal as possible.
21
21
22
22
Then, when cells are placed, Chops are created so that they produce as similar
23
23
cells to the calculated as possible. Since blockMesh only supports equal
@@ -32,10 +32,10 @@ class DistributorBase(abc.ABC):
32
32
33
33
@staticmethod
34
34
def get_actual_ratios (coords : FloatListType ) -> FloatListType :
35
+ # TODO: repeated code within Approximator! un-repeat
35
36
lengths = np .diff (coords )
36
- return ( lengths [: - 1 ] / np .roll (lengths , - 1 )[:- 1 ]) ** - 1
37
+ return np .roll (lengths , - 1 )[:- 1 ] / lengths [: - 1 ]
37
38
38
- @abc .abstractmethod
39
39
def get_ideal_ratios (self ) -> FloatListType :
40
40
"""Returns desired cell-to-cell ratios"""
41
41
return np .ones (self .count + 1 )
@@ -45,7 +45,9 @@ def get_ratio_weights(self) -> FloatListType:
45
45
"""Returns weights of cell ratios"""
46
46
47
47
def get_raw_coords (self ) -> FloatListType :
48
- # 'count' denotes number of 'intervals' so there must be another point
48
+ # 'count' denotes number of 'intervals' so add one more point to get points;
49
+ # first and last cells are added artificially ('ghost cells') to calculate
50
+ # proper expansion ratios and sizes
49
51
return np .concatenate (
50
52
([- self .size_before ], np .linspace (0 , self .length , num = self .count + 1 ), [self .length + self .size_after ])
51
53
)
@@ -60,69 +62,31 @@ def get_smooth_coords(self) -> FloatListType:
60
62
def ratios (inner_coords ):
61
63
coords [2 :- 2 ] = inner_coords
62
64
63
- difference = self .get_actual_ratios (coords ) - self .get_ideal_ratios ()
64
- return difference * self .get_ratio_weights ()
65
+ # to prevent 'flipping' over and producing zero or negative length, scale with e^-ratio
66
+ difference = - (self .get_ratio_weights () * (self .get_actual_ratios (coords ) - self .get_ideal_ratios ()))
67
+ return np .exp (difference ) - 1
65
68
66
- scale = min (self .size_before , self .size_after , self .length / self .count ) / 10
67
- _ = scipy .optimize .least_squares (ratios , coords [2 :- 2 ], method = "lm" , ftol = scale / 100 , x_scale = scale )
69
+ scale = min (self .size_before , self .size_after , self .length / self .count ) / 100
70
+ _ = scipy .optimize .least_squares (ratios , coords [2 :- 2 ], ftol = scale / 10 , x_scale = scale )
68
71
69
- return coords
72
+ # omit the 'ghost' cells
73
+ return coords [1 :- 1 ]
70
74
71
75
@property
72
76
def is_simple (self ) -> bool :
73
77
# Don't overdo basic, simple-graded blocks
74
78
base_size = self .length / self .count
75
79
76
80
# TODO: use a more relaxed criterion?
77
- return base_size - self .size_before < TOL and base_size - self .size_after < TOL
81
+ return base_size - self .size_before < TOL and base_size - self .size_after < 10 * TOL
78
82
79
83
def get_chops (self , pieces : int ) -> List [Chop ]:
80
- if self .is_simple :
81
- return [ Chop ( count = self . count )]
84
+ approximator = Approximator ( self .get_smooth_coords ())
85
+ return approximator . get_chops ( pieces )
82
86
87
+ def get_last_size (self ) -> float :
83
88
coords = self .get_smooth_coords ()
84
- sizes = np .diff (coords )
85
- ratios = self .get_actual_ratios (coords )
86
-
87
- count = len (ratios ) - 1
88
-
89
- # create a piecewise linear function from a number of chosen indexes and their respective c2c ratios
90
- # then optimize indexes to obtain best fit
91
- # fratios = scipy.interpolate.interp1d(range(len(ratios)), ratios)
92
-
93
- # def get_piecewise(indexes: List[int]) -> Callable:
94
- # values = np.take(ratios, indexes)
95
- # return scipy.interpolate.interp1d(indexes, values)
96
-
97
- # def get_fitness(indexes: List[int]) -> float:
98
- # fitted = get_piecewise(indexes)(range(len(ratios)))
99
-
100
- # ss_tot = np.sum((ratios - np.mean(ratios)) ** 2)
101
- # ss_res = np.sum((ratios - fitted) ** 2)
102
-
103
- # return 1 - (ss_res / ss_tot)
104
-
105
- # print(get_fitness([0, 9, 10, count]))
106
- # print(get_fitness(np.linspace(0, count, num=pieces + 1, dtype=int)))
107
-
108
- chops : List [Chop ] = []
109
- indexes = np .linspace (0 , count , num = pieces + 1 , dtype = int )
110
-
111
- for i , index in enumerate (indexes [:- 1 ]):
112
- chop_ratios = ratios [index : indexes [i + 1 ]]
113
- ratio = np .prod (chop_ratios )
114
- chop_count = indexes [i + 1 ] - indexes [i ]
115
- avg_ratio = ratio ** (1 / chop_count )
116
- length = sum_length (sizes [index ], chop_count , avg_ratio )
117
- chop = Chop (length_ratio = length , total_expansion = ratio , count = chop_count )
118
- chops .append (chop )
119
-
120
- # normalize length ratios
121
- sum_ratio = sum ([chop .length_ratio for chop in chops ])
122
- for chop in chops :
123
- chop .length_ratio = chop .length_ratio / sum_ratio
124
-
125
- return chops
89
+ return coords [- 2 ] - coords [- 3 ]
126
90
127
91
128
92
@dataclasses .dataclass
@@ -132,8 +96,9 @@ def get_ideal_ratios(self):
132
96
return super ().get_ideal_ratios ()
133
97
134
98
def get_ratio_weights (self ):
135
- # Enforce stricter policy on size_before and size_after
136
99
weights = np .ones (self .count + 1 )
100
+ # Enforce stricter policy on the first few cells
101
+ # to match size_before and size_after
137
102
for i in (0 , 1 , 2 , 3 ):
138
103
w = 2 ** (3 - i )
139
104
weights [i ] = w
@@ -146,26 +111,34 @@ def get_ratio_weights(self):
146
111
class InflationDistributor (SmoothDistributor ):
147
112
c2c_expansion : float
148
113
bl_thickness_factor : int
114
+ buffer_expansion : float
115
+ bulk_size : float
149
116
150
117
@property
151
118
def is_simple (self ) -> bool :
152
119
return False
153
120
154
121
def get_ideal_ratios (self ):
122
+ # TODO: combine this logic and LayerStack;
123
+ # possibly package all parameters into a separate dataclass
124
+ ratios = super ().get_ideal_ratios ()
125
+
155
126
# Ideal growth ratio in boundary layer is user-specified c2c_expansion;
156
127
inflation_layer = InflationLayer (self .size_before , self .c2c_expansion , self .bl_thickness_factor , 1e12 )
157
128
inflation_count = inflation_layer .count
158
- print (f"Inflation count: { inflation_count } " )
159
-
160
- ratios = super ().get_ideal_ratios ()
161
129
162
130
ratios [:inflation_count ] = self .c2c_expansion
163
- print (ratios )
131
+
132
+ # add a buffer layer if needed
133
+ last_inflation_size = inflation_layer .end_size
134
+ if self .bulk_size > self .buffer_expansion * last_inflation_size :
135
+ buffer_count = int (np .log (self .bulk_size / last_inflation_size ) / np .log (self .buffer_expansion )) + 1
136
+ ratios [inflation_count : inflation_count + buffer_count ] = self .buffer_expansion
164
137
165
138
return ratios
166
139
167
140
def get_ratio_weights (self ):
168
- return super (). get_ratio_weights ()
169
-
170
- def _get_ratio_weights ( self ):
141
+ # using the same weights as in SmoothDistributor
142
+ # can trigger overflow warnings but doesn't produce
143
+ # better chops; thus, keep it simple
171
144
return np .ones (self .count + 1 )
0 commit comments