Skip to content

Commit 9f3c033

Browse files
committed
Improve optimization
- handle concave quads (TODO: refactor the function) - use under-relaxation (TODO: rewrite to use actual number instead of bool) - use RMS value when summing up quality of junctions - handle crashes when calculating sensitivity outside bounds (TODO: handle bounds within the calculation) - use a different transform function for angles (does not go to infinity and is non-linear below some threshold)
1 parent 7d64d48 commit 9f3c033

File tree

7 files changed

+91
-26
lines changed

7 files changed

+91
-26
lines changed
7.94 KB
Binary file not shown.

examples/complex/airfoil/airfoil.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import classy_blocks as cb
66
from classy_blocks.util import functions as f
77

8+
# TODO! Rewrite this to use a MappedSketch and get rid of 3D optimization of a 2D domain
9+
810
# This rather lengthy tutorial does the following:
911
# - reads 2D airfoil points*
1012
# - rotates the airfoil to a desired angle of attack

examples/complex/cyclone/cyclone.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def add_regions(regions: List[Region]) -> None:
8080
for clamp in region.get_clamps(mesh):
8181
optimizer.add_clamp(clamp)
8282

83-
optimizer.optimize(tolerance=1e-3, method="SLSQP")
83+
optimizer.optimize(tolerance=1e-3, relax=True, max_iterations=10)
8484

8585
# Now Block objects contain optimization results but those are not reflected in
8686
# user-created Operations. Mesh.backport() will copy the data back.

src/classy_blocks/optimize/grid.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def quality(self) -> float:
127127
"""Returns summed qualities of all junctions"""
128128
# It is only called when optimizing linked clamps
129129
# or at the end of an iteration.
130-
return sum(junction.quality for junction in self.junctions)
130+
return sum(junction.quality**2 for junction in self.junctions) ** 0.5
131131

132132
def update(self, index: int, position: NPPointType) -> float:
133133
self.points[index] = position

src/classy_blocks/optimize/junction.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,7 @@ def quality(self) -> float:
6262
else:
6363
quality_function = get_quad_quality
6464

65-
return sum(quality_function(self.points, np.array(cell.indexes, dtype=np.int32)) for cell in self.cells)
65+
return (
66+
sum(quality_function(self.points, np.array(cell.indexes, dtype=np.int32)) ** 2 for cell in self.cells)
67+
** 0.5
68+
)

src/classy_blocks/optimize/optimizer.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def add_clamp(self, clamp: ClampBase) -> None:
3434
def add_link(self, link: LinkBase) -> None:
3535
self.grid.add_link(link)
3636

37-
def optimize_clamp(self, clamp: ClampBase, method: MinimizationMethodType) -> None:
37+
def optimize_clamp(self, clamp: ClampBase, method: MinimizationMethodType, relax: bool, iteration_no: int) -> None:
3838
"""Move clamp.vertex so that quality at junction is improved;
3939
rollback changes if grid quality decreased after optimization"""
4040
initial_params = copy.copy(clamp.params)
@@ -46,11 +46,23 @@ def optimize_clamp(self, clamp: ClampBase, method: MinimizationMethodType) -> No
4646
def fquality(params):
4747
# move all vertices according to X
4848
clamp.update_params(params)
49-
return self.grid.update(junction.index, clamp.position)
49+
quality = self.grid.update(junction.index, clamp.position)
50+
return quality
5051

5152
try:
5253
scipy.optimize.minimize(fquality, clamp.params, bounds=clamp.bounds, method=method, tol=1e-2)
5354

55+
if relax:
56+
relaxation_factor = 1 - 2 ** -(iteration_no + 1)
57+
if relaxation_factor > 0.9:
58+
relaxation_factor = 1
59+
60+
for i, param in enumerate(clamp.params):
61+
clamp.params[i] = initial_params[i] + relaxation_factor * (param - initial_params[i])
62+
63+
# update the position after relaxation
64+
fquality(clamp.params)
65+
5466
reporter.junction_final = junction.quality
5567
reporter.grid_final = self.grid.quality
5668

@@ -78,36 +90,45 @@ def fquality(clamp, junction, params):
7890
self.grid.update(junction.index, clamp.position)
7991
return junction.quality
8092

81-
sensitivities = np.asarray(
82-
scipy.optimize.approx_fprime(clamp.params, lambda p: fquality(clamp, junction, p), epsilon=10 * TOL)
83-
)
93+
try:
94+
sensitivities = np.asarray(
95+
scipy.optimize.approx_fprime(clamp.params, lambda p: fquality(clamp, junction, p), epsilon=10 * TOL)
96+
)
97+
except ValueError:
98+
clamp.update_params(initial_params)
99+
self.grid.update(junction.index, clamp.position)
100+
return np.ones(len(clamp.params))
84101

85102
clamp.update_params(initial_params)
86103
self.grid.update(junction.index, clamp.position)
87104

88105
return np.linalg.norm(sensitivities)
89106

90-
def optimize_iteration(self, method: MinimizationMethodType) -> None:
107+
def optimize_iteration(self, method: MinimizationMethodType, relax: bool, iteration_no: int) -> None:
91108
clamps = sorted(self.grid.clamps, key=lambda c: self._get_sensitivity(c), reverse=True)
92109

93110
for clamp in clamps:
94-
self.optimize_clamp(clamp, method)
111+
self.optimize_clamp(clamp, method, relax, iteration_no)
95112

96113
def optimize(
97-
self, max_iterations: int = 20, tolerance: float = 0.1, method: MinimizationMethodType = "SLSQP"
114+
self,
115+
max_iterations: int = 20,
116+
tolerance: float = 0.1,
117+
method: MinimizationMethodType = "SLSQP",
118+
relax: bool = False,
98119
) -> IterationDriver:
99120
"""Move vertices, defined and restrained with Clamps
100121
so that better mesh quality is obtained.
101122
102123
Within each iteration, all vertices will be moved, starting with the one with the most influence on quality.
103-
Lower tolerance values"""
124+
Lower tolerance values."""
104125
driver = IterationDriver(max_iterations, tolerance)
105126

106127
start_time = time.time()
107128

108129
while not driver.converged:
109130
driver.begin_iteration(self.grid.quality)
110-
self.optimize_iteration(method)
131+
self.optimize_iteration(method, relax, len(driver.iterations))
111132
driver.end_iteration(self.grid.quality)
112133

113134
end_time = time.time()

src/classy_blocks/optimize/quality.py

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@ def scale_quality(base: float, exponent: float, factor: float, value: float) ->
1717

1818
@numba.jit(nopython=True, cache=True)
1919
def scale_non_ortho(angle: float) -> float:
20-
return scale_quality(1.5, 0.25, 0.5, angle)
20+
return scale_quality(1.4, 0.25, 0.5, angle)
2121

2222

2323
@numba.jit(nopython=True, cache=True)
2424
def scale_inner_angle(angle: float) -> float:
25-
return scale_quality(1.5, 0.25, 0.5, np.abs(angle))
25+
return scale_quality(1.4, 0.25, 0.5, np.abs(angle))
2626

2727

2828
@numba.jit(nopython=True, cache=True)
2929
def scale_aspect(ratio: float) -> float:
30-
return scale_quality(3, 2.5, 3, np.log10(ratio))
30+
return scale_quality(4, 3, 3, np.log10(ratio))
3131

3232

3333
@numba.jit(nopython=True, cache=True)
@@ -73,6 +73,41 @@ def get_quad_normal(points: NPPointListType) -> Tuple[NPVectorType, NPVectorType
7373
return center, normal / np.linalg.norm(normal), max_length / (min_length + VSMALL)
7474

7575

76+
@numba.jit(nopython=True, cache=True)
77+
def is_quad_convex(points: NPPointListType) -> bool:
78+
# Compute normal using the first three points
79+
normal = np.cross(points[1] - points[0], points[2] - points[1])
80+
normal /= np.linalg.norm(normal)
81+
82+
sign = 0
83+
for i in range(4):
84+
prev_leg = points[i] - points[(i - 1) % 4]
85+
next_leg = points[(i + 1) % 4] - points[i]
86+
cross = np.cross(prev_leg, next_leg)
87+
dot = np.dot(cross, normal)
88+
if i == 0:
89+
sign = np.sign(dot)
90+
if sign == 0:
91+
return False # Degenerate
92+
else:
93+
if np.sign(dot) != sign:
94+
return False
95+
return True
96+
97+
98+
@numba.jit(nopython=True, cache=True)
99+
def scale_angle(angle: float) -> float:
100+
n = 4
101+
m = 10
102+
threshold = 65
103+
a = m / (n * threshold ** (n - 1))
104+
105+
if angle <= threshold:
106+
return a * angle**n
107+
108+
return a * threshold**n + m * (angle - threshold)
109+
110+
76111
@numba.jit(nopython=True, cache=True)
77112
def get_quad_non_ortho_quality(
78113
quad_points: NPPointListType, quad_center: NPPointType, quad_normal: NPPointType
@@ -92,7 +127,9 @@ def get_quad_non_ortho_quality(
92127
center_vector /= np.linalg.norm(center_vector)
93128

94129
angle = 180 * np.arccos(np.dot(side_normal, center_vector)) / np.pi
95-
quality += scale_non_ortho(angle)
130+
if not is_quad_convex(quad_points):
131+
angle += 180
132+
quality += scale_angle(angle)
96133

97134
return quality
98135

@@ -113,7 +150,10 @@ def get_quad_angle_quality(quad_points: NPPointListType) -> float:
113150
side_2 /= np.linalg.norm(side_2) + VSMALL
114151

115152
angle = 180 * np.arccos(np.dot(side_1, side_2)) / np.pi - 90
116-
quality += scale_inner_angle(angle)
153+
if not is_quad_convex(quad_points):
154+
angle += 180
155+
156+
quality += scale_angle(angle)
117157

118158
return quality
119159

@@ -129,6 +169,7 @@ def get_quad_quality(grid_points: NPPointListType, cell_indexes: NPIndexType) ->
129169
# inner angles
130170
quality += get_quad_angle_quality(cell_points)
131171

172+
# aspect ratio
132173
quality += scale_aspect(cell_aspect)
133174

134175
return quality
@@ -141,8 +182,6 @@ def get_hex_quality(grid_points: NPPointListType, cell_indexes: NPIndexType) ->
141182

142183
side_indexes = np.array([[0, 1, 2, 3], [7, 6, 5, 4], [4, 0, 3, 7], [6, 2, 1, 5], [0, 4, 5, 1], [7, 3, 2, 6]])
143184

144-
max_aspect = 1
145-
146185
quality = 0
147186

148187
for side in side_indexes:
@@ -158,14 +197,14 @@ def get_hex_quality(grid_points: NPPointListType, cell_indexes: NPIndexType) ->
158197

159198
center_vector /= np.linalg.norm(center_vector)
160199

161-
angle = 180 * np.arccos(np.dot(side_normal, center_vector)) / np.pi
162-
quality += scale_non_ortho(angle)
163-
164-
max_aspect = max(max_aspect, side_aspect)
200+
angle = 180 * np.arccos(min(1 - VSMALL, np.dot(side_normal, center_vector))) / np.pi
201+
if not is_quad_convex(side_points):
202+
angle = 180 - angle
203+
quality += scale_angle(angle)
165204

166-
# take inner angles and aspect from quad calculation
205+
# take inner angles and aspect from quad calculation;
167206
quality += get_quad_angle_quality(side_points)
168207

169-
quality += scale_aspect(max_aspect)
208+
quality += scale_aspect(side_aspect)
170209

171210
return quality

0 commit comments

Comments
 (0)