Skip to content

Commit 6403752

Browse files
committed
Merge branch 'development' into feature/autochop
2 parents 039ed67 + c70979e commit 6403752

File tree

17 files changed

+350
-21
lines changed

17 files changed

+350
-21
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,13 @@ Airfoil core with blunt trailing edge (imported points from NACA generator) and
367367
(see `examples/complex/airfoil.py`). A simulation-ready mesh needs additional blocks to expand domain further away from the airfoil.
368368
![Airfoil](showcase/airfoil.png "Airfoil core mesh")
369369

370+
## Automatic Edge Grading
371+
When setting cell counts and expansion ratios, it is possible to specify which value to keep constant. Mostly this will be used for keeping thickness of the first cell at the wall consistent to maintain desired `y+` throughout the mesh. This is done by simple specifying a `preserve="..."` keyword.
372+
373+
Example: a block chopped in a classic way where cell sizes will increase when block size increases:
374+
![Classic block grading](showcase/classy_classic_grading.png "Classic block grading")
375+
The same case but with a specified `preserve="start_size"` keyword for the bottom and `preserve="end_size"` for the top edge:
376+
![Grading for consistent cell size](showcase/classy_edges_grading.png "Classy block grading")
370377

371378
## Debugging
372379

examples/optimization/duct.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# An example where a Shape is optimized *before* it is added to mesh, using ShapeOptimizer
2+
import os
3+
4+
import classy_blocks as cb
5+
from classy_blocks.optimize.junction import ClampExistsError
6+
from classy_blocks.optimize.optimizer import ShapeOptimizer
7+
8+
mesh = cb.Mesh()
9+
10+
start_sketch = cb.SplineDisk([0, 0, 0], [2.5, 0, 0], [0, 1, 0], 0, 0)
11+
end_sketch = cb.SplineDisk([0, 0, 0], [1, 0, 0], [0, 2.5, 0], 0, 0).translate([0, 0, 1])
12+
13+
shape = cb.LoftedShape(start_sketch, end_sketch)
14+
15+
optimizer = ShapeOptimizer(shape.operations)
16+
17+
for operation in shape.operations[:4]:
18+
# remove edges because inner splines will ruin the day
19+
# TODO: make edges move with points too
20+
operation.top_face.remove_edges()
21+
operation.bottom_face.remove_edges()
22+
23+
for point in operation.point_array:
24+
try:
25+
optimizer.add_clamp(cb.FreeClamp(point))
26+
except ClampExistsError:
27+
pass
28+
29+
optimizer.optimize(tolerance=0.01)
30+
31+
# Quick'n'dirty chopping, don't do this at home
32+
for operation in shape.operations:
33+
for axis in range(3):
34+
operation.chop(axis, count=10)
35+
36+
mesh.add(shape)
37+
38+
mesh.set_default_patch("walls", "wall")
39+
mesh.write(os.path.join("..", "case", "system", "blockMeshDict"), debug_path="debug.vtk")

src/classy_blocks/construct/curves/interpolated.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ class InterpolatedCurveBase(FunctionCurveBase, abc.ABC):
2525

2626
_interpolator: Type[InterpolatorBase]
2727

28-
def __init__(self, points: PointListType):
28+
def __init__(self, points: PointListType, extrapolate: bool = False, equalize: bool = True):
2929
self.array = Array(points)
30-
self.function = self._interpolator(self.array, False)
30+
self.function = self._interpolator(self.array, extrapolate, equalize)
3131
self.bounds = (0, 1)
3232

3333
@property

src/classy_blocks/construct/curves/interpolators.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33
import numpy as np
44
import scipy.interpolate
5-
from numpy.typing import NDArray
65

76
from classy_blocks.construct.array import Array
8-
from classy_blocks.types import NPPointType, ParamCurveFuncType
7+
from classy_blocks.types import FloatListType, NPPointType, ParamCurveFuncType
98

109

1110
class InterpolatorBase(abc.ABC):
@@ -19,9 +18,10 @@ class InterpolatorBase(abc.ABC):
1918
def _get_function(self) -> ParamCurveFuncType:
2019
"""Returns an interpolation function from stored points"""
2120

22-
def __init__(self, points: Array, extrapolate: bool):
21+
def __init__(self, points: Array, extrapolate: bool, equalize: bool = True):
2322
self.points = points
2423
self.extrapolate = extrapolate
24+
self.equalize = equalize
2525

2626
self.function = self._get_function()
2727
self._valid = True
@@ -37,7 +37,16 @@ def invalidate(self) -> None:
3737
self._valid = False
3838

3939
@property
40-
def params(self) -> NDArray:
40+
def params(self) -> FloatListType:
41+
"""A list of parameters for the interpolation curve.
42+
If not equalized, it's just linearly spaced floats;
43+
if equalized, scaled distances between provided points are taken so that
44+
evenly spaced parameters will produce evenly spaced points even if
45+
interpolation points are unequally spaced."""
46+
if self.equalize:
47+
lengths = np.cumsum(np.sqrt(np.sum((self.points[:-1] - self.points[1:]) ** 2, axis=1)))
48+
return np.concatenate(([0], lengths / lengths[-1]))
49+
4150
return np.linspace(0, 1, num=len(self.points))
4251

4352

src/classy_blocks/construct/flat/face.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def project_edge(self, corner: int, label: ProjectToType) -> None:
105105
def invert(self) -> "Face":
106106
"""Reverses the order of points in this face."""
107107
self.points.reverse()
108+
self.edges.reverse()
109+
self.edges = [self.edges[i] for i in (1, 2, 3, 0)]
108110

109111
return self
110112

@@ -168,8 +170,7 @@ def project(self, label: str, edges: bool = False, points: bool = False) -> None
168170
self.points[i].project(label)
169171

170172
def shift(self, count: int) -> "Face":
171-
"""Shifts points of this face by 'count', changing its
172-
starting point"""
173+
"""Shifts points of this face by 'count', changing its starting point"""
173174
indexes = collections.deque(range(4))
174175
indexes.rotate(count)
175176

src/classy_blocks/construct/flat/sketches/spline_round.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ def __init__(
336336
@property
337337
def grid(self) -> List[List[Face]]:
338338
if len(self.faces) > 6:
339-
return [self.faces[:4], self.faces[4:]]
339+
return [self.faces[::3], [face for i, face in enumerate(self.faces) if not i % 3 == 0]]
340340
else:
341341
return super().grid
342342

src/classy_blocks/grading/autograding/probe.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import functools
2-
from typing import Dict, List, Optional, get_args
2+
from typing import Dict, List, Optional, Set, get_args
33

44
from classy_blocks.base.exceptions import BlockNotFoundError, NoInstructionError
55
from classy_blocks.items.block import Block
6+
from classy_blocks.items.vertex import Vertex
67
from classy_blocks.items.wires.axis import Axis
78
from classy_blocks.items.wires.wire import Wire
89
from classy_blocks.mesh import Mesh
9-
from classy_blocks.types import ChopTakeType, DirectionType
10+
from classy_blocks.optimize.grid import HexGrid
11+
from classy_blocks.types import ChopTakeType, DirectionType, OrientType
12+
from classy_blocks.util.constants import FACE_MAP
1013

1114

1215
@functools.lru_cache(maxsize=3000) # that's for 1000 blocks
@@ -18,6 +21,20 @@ def get_block_from_axis(mesh: Mesh, axis: Axis) -> Block:
1821
raise RuntimeError("Block for Axis not found!")
1922

2023

24+
@functools.lru_cache(maxsize=2)
25+
def get_defined_wall_vertices(mesh: Mesh) -> Set[Vertex]:
26+
"""Returns vertices that are on the 'wall' patches"""
27+
wall_vertices: set[Vertex] = set()
28+
29+
# explicitly defined walls
30+
for patch in mesh.patches:
31+
if patch.kind == "wall":
32+
for side in patch.sides:
33+
wall_vertices.update(set(side.vertices))
34+
35+
return wall_vertices
36+
37+
2138
class Instruction:
2239
"""A descriptor that tells in which direction the specific block can be chopped."""
2340

@@ -151,10 +168,46 @@ class Probe:
151168

152169
def __init__(self, mesh: Mesh):
153170
self.mesh = mesh
171+
172+
# maps blocks to rows
154173
self.catalogue = Catalogue(self.mesh)
155174

175+
# finds blocks' neighbours
176+
self.grid = HexGrid.from_mesh(self.mesh)
177+
156178
def get_row_blocks(self, block: Block, direction: DirectionType) -> List[Block]:
157179
return self.catalogue.get_row_blocks(block, direction)
158180

159181
def get_rows(self, direction: DirectionType) -> List[Row]:
160182
return self.catalogue.rows[direction]
183+
184+
def get_explicit_wall_vertices(self, block: Block) -> Set[Vertex]:
185+
"""Returns vertices from a block that lie on explicitly defined wall patches"""
186+
mesh_vertices = get_defined_wall_vertices(self.mesh)
187+
block_vertices = set(block.vertices)
188+
189+
return block_vertices.intersection(mesh_vertices)
190+
191+
def get_default_wall_vertices(self, block: Block) -> Set[Vertex]:
192+
"""Returns vertices that lie on default 'wall' patch"""
193+
wall_vertices: Set[Vertex] = set()
194+
195+
# other sides when mesh has a default wall patch
196+
if self.mesh.patch_list.default["kind"] == "wall":
197+
# find block boundaries
198+
block_index = self.mesh.blocks.index(block)
199+
cell = self.grid.cells[block_index]
200+
201+
# sides with no neighbours are on boundary
202+
boundaries: List[OrientType] = [
203+
orient for orient, neighbours in cell.neighbours.items() if neighbours is None
204+
]
205+
206+
for orient in boundaries:
207+
wall_vertices.union({block.vertices[i] for i in FACE_MAP[orient]})
208+
209+
return wall_vertices
210+
211+
def get_wall_vertices(self, block: Block) -> Set[Vertex]:
212+
"""Returns vertices that are on the 'wall' patches"""
213+
return self.get_explicit_wall_vertices(block).union(self.get_default_wall_vertices(block))

src/classy_blocks/mesh.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from classy_blocks.construct.shape import Shape
99
from classy_blocks.construct.stack import Stack
1010
from classy_blocks.items.block import Block
11+
from classy_blocks.items.patch import Patch
1112
from classy_blocks.items.vertex import Vertex
1213
from classy_blocks.lists.block_list import BlockList
1314
from classy_blocks.lists.edge_list import EdgeList
@@ -243,6 +244,10 @@ def is_assembled(self) -> bool:
243244
def vertices(self) -> List[Vertex]:
244245
return self.vertex_list.vertices
245246

247+
@property
248+
def patches(self) -> List[Patch]:
249+
return list(self.patch_list.patches.values())
250+
246251
@property
247252
def operations(self) -> List[Operation]:
248253
"""Returns a list of operations from all entities in depot"""

src/classy_blocks/optimize/cell.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,19 @@ def q_scale(base, exponent, factor, value):
164164
quality += np.sum(q_scale(3, 2.5, 3, aspect_factor))
165165

166166
except RuntimeWarning:
167-
raise ValueError("Degenerate Cell") from RuntimeWarning
167+
raise ValueError(f"Degenerate Cell: {self}") from RuntimeWarning
168168
finally:
169169
warnings.resetwarnings()
170170

171171
return quality
172172

173+
@property
174+
def min_length(self) -> float:
175+
return min(self.get_edge_lengths())
176+
177+
def __str__(self):
178+
return "-".join([str(index) for index in self.indexes])
179+
173180

174181
class QuadCell(CellBase):
175182
# Like constants.FACE_MAP but for quadrangle sides as line segments

src/classy_blocks/optimize/grid.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
from typing import List, Type
1+
from typing import List, Type, Union
22

33
import numpy as np
44

55
from classy_blocks.base.exceptions import InvalidLinkError, NoJunctionError
6+
from classy_blocks.construct.assemblies.assembly import Assembly
7+
from classy_blocks.construct.flat.sketch import Sketch
68
from classy_blocks.construct.flat.sketches.mapped import MappedSketch
9+
from classy_blocks.construct.operations.operation import Operation
10+
from classy_blocks.construct.shape import Shape
11+
from classy_blocks.construct.stack import Stack
712
from classy_blocks.mesh import Mesh
813
from classy_blocks.optimize.cell import CellBase, HexCell, QuadCell
914
from classy_blocks.optimize.clamps.clamp import ClampBase
1015
from classy_blocks.optimize.junction import Junction
1116
from classy_blocks.optimize.links import LinkBase
17+
from classy_blocks.optimize.mapper import Mapper
1218
from classy_blocks.types import IndexType, NPPointListType, NPPointType
1319
from classy_blocks.util import functions as f
1420
from classy_blocks.util.constants import TOL
@@ -124,16 +130,41 @@ class QuadGrid(GridBase):
124130
cell_class = QuadCell
125131

126132
@classmethod
127-
def from_sketch(cls, sketch: MappedSketch) -> "QuadGrid":
128-
# TODO: make grids from ANY sketch
129-
return QuadGrid(sketch.positions, sketch.indexes)
133+
def from_sketch(cls, sketch: Sketch) -> "QuadGrid":
134+
if isinstance(sketch, MappedSketch):
135+
# Use the mapper's indexes (provided by the user!)
136+
return cls(sketch.positions, sketch.indexes)
137+
138+
# automatically create a mapping for arbitrary sketches
139+
mapper = Mapper()
140+
for face in sketch.faces:
141+
mapper.add(face)
142+
143+
return cls(np.array(mapper.points), mapper.indexes)
130144

131145

132146
class HexGrid(GridBase):
133147
cell_class = HexCell
134148

149+
@classmethod
150+
def from_elements(cls, elements: List[Union[Operation, Shape, Stack, Assembly]]) -> "HexGrid":
151+
"""Creates a grid from a list of elements"""
152+
mapper = Mapper()
153+
154+
for element in elements:
155+
if isinstance(element, Operation):
156+
operations = [element]
157+
else:
158+
operations = element.operations
159+
160+
for operation in operations:
161+
mapper.add(operation)
162+
163+
return cls(np.array(mapper.points), mapper.indexes)
164+
135165
@classmethod
136166
def from_mesh(cls, mesh: Mesh) -> "HexGrid":
167+
"""Creates a grid from an assembled Mesh object"""
137168
points = np.array([vertex.position for vertex in mesh.vertices])
138169
addresses = [block.indexes for block in mesh.blocks]
139170

0 commit comments

Comments
 (0)