Skip to content

Commit 536b0c4

Browse files
committed
Merge with feature/shape_optimization
2 parents 3cd87b3 + 22d5ae0 commit 536b0c4

File tree

5 files changed

+189
-5
lines changed

5 files changed

+189
-5
lines changed

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/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

src/classy_blocks/optimize/mapper.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from typing import List, Union
2+
3+
from classy_blocks.construct.flat.face import Face
4+
from classy_blocks.construct.operations.operation import Operation
5+
from classy_blocks.types import IndexType, NPPointType
6+
from classy_blocks.util import functions as f
7+
from classy_blocks.util.constants import TOL
8+
9+
ElementType = Union[Face, Operation]
10+
11+
12+
class Mapper:
13+
"""A helper that constructs mapped sketches/shapes
14+
from arbitrary collection of faces/operations"""
15+
16+
def __init__(self) -> None:
17+
self.points: List[NPPointType] = []
18+
self.indexes: List[IndexType] = []
19+
self.elements: List[Union[Face, Operation]] = []
20+
21+
def _add_point(self, point: NPPointType) -> int:
22+
# TODO: this code is repeated several times all over;
23+
# consolidate, unify, agglomerate, amass
24+
# (especially in case one would need to utilize an octree or something)
25+
for i, position in enumerate(self.points):
26+
if f.norm(point - position) < TOL:
27+
# reuse an existing point
28+
index = i
29+
break
30+
else:
31+
# no point found, create a new one
32+
index = len(self.points)
33+
self.points.append(point)
34+
35+
return index
36+
37+
def add(self, element: ElementType) -> None:
38+
"""Add Face's or Operation's points to the map"""
39+
indexes = [self._add_point(point) for point in element.point_array]
40+
self.indexes.append(indexes)
41+
self.elements.append(element)
42+
43+
@classmethod
44+
def from_map(cls, points: List[NPPointType], indexes: List[IndexType], elements: List[ElementType]) -> "Mapper":
45+
"""Creates a ready-made mapper from a sketch/shape that already has points/indexes defined"""
46+
if len(indexes) != len(elements):
47+
raise ValueError("Number of indexes and elements don't match!")
48+
49+
mapper = cls()
50+
mapper.points = points
51+
mapper.indexes = indexes
52+
mapper.elements = elements
53+
54+
return mapper

src/classy_blocks/optimize/optimizer.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import abc
22
import copy
33
import time
4-
from typing import Literal
4+
from typing import List, Literal
55

66
import numpy as np
77
import scipy.optimize
88

99
from classy_blocks.construct.flat.sketches.mapped import MappedSketch
10+
from classy_blocks.construct.operations.operation import Operation
1011
from classy_blocks.mesh import Mesh
1112
from classy_blocks.optimize.clamps.clamp import ClampBase
1213
from classy_blocks.optimize.clamps.surface import PlaneClamp
1314
from classy_blocks.optimize.grid import GridBase, HexGrid, QuadGrid
1415
from classy_blocks.optimize.iteration import ClampOptimizationData, IterationDriver
1516
from classy_blocks.optimize.links import LinkBase
17+
from classy_blocks.optimize.mapper import Mapper
1618
from classy_blocks.util.constants import TOL
1719

1820
MinimizationMethodType = Literal["SLSQP", "L-BFGS-B", "Nelder-Mead", "Powell"]
@@ -144,6 +146,26 @@ def backport(self):
144146
self.mesh.vertices[i].move_to(point)
145147

146148

149+
class ShapeOptimizer(OptimizerBase):
150+
def __init__(self, operations: List[Operation], report: bool = True):
151+
self.mapper = Mapper()
152+
153+
for operation in operations:
154+
self.mapper.add(operation)
155+
156+
grid = HexGrid(np.array(self.mapper.points), self.mapper.indexes)
157+
158+
super().__init__(grid, report)
159+
160+
def backport(self) -> None:
161+
# Move every point of every operation to wherever it is now
162+
for iop, indexes in enumerate(self.mapper.indexes):
163+
operation = self.mapper.elements[iop]
164+
165+
for ipnt, i in enumerate(indexes):
166+
operation.points[ipnt].move_to(self.grid.points[i])
167+
168+
147169
class SketchOptimizer(OptimizerBase):
148170
def __init__(self, sketch: MappedSketch, report: bool = True):
149171
self.sketch = sketch

tests/test_optimize/test_mapper.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import unittest
2+
3+
from classy_blocks.construct.flat.face import Face
4+
from classy_blocks.construct.operations.box import Box
5+
from classy_blocks.optimize.mapper import Mapper
6+
7+
8+
class MapperTests(unittest.TestCase):
9+
def test_single_face(self):
10+
mapper = Mapper()
11+
mapper.add(Face([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]]))
12+
13+
self.assertListEqual(mapper.indexes, [[0, 1, 2, 3]])
14+
15+
def test_two_faces(self):
16+
mapper = Mapper()
17+
face = Face([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]])
18+
mapper.add(face)
19+
mapper.add(face.copy().translate([1, 0, 0]))
20+
21+
self.assertListEqual(mapper.indexes, [[0, 1, 2, 3], [1, 4, 5, 2]])
22+
23+
def test_single_operation(self):
24+
mapper = Mapper()
25+
box = Box([0, 0, 0], [1, 1, 1])
26+
27+
mapper.add(box)
28+
29+
self.assertListEqual(mapper.indexes, [[0, 1, 2, 3, 4, 5, 6, 7]])
30+
31+
def test_two_operations(self):
32+
mapper = Mapper()
33+
box = Box([0, 0, 0], [1, 1, 1])
34+
35+
mapper.add(box)
36+
mapper.add(box.copy().translate([1, 0, 0]))
37+
38+
self.assertListEqual(mapper.indexes, [[0, 1, 2, 3, 4, 5, 6, 7], [1, 8, 9, 2, 5, 10, 11, 6]])

0 commit comments

Comments
 (0)