Skip to content

Commit 33a4208

Browse files
authored
new method: generate_obj (#78)
1 parent 905eb8f commit 33a4208

File tree

2 files changed

+218
-51
lines changed

2 files changed

+218
-51
lines changed

Diff for: bladex/propeller.py

+120-13
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Fuse
88
from OCC.Extend.DataExchange import write_stl_file
99
from OCC.Display.SimpleGui import init_display
10+
from smithers.io.obj import ObjHandler, WavefrontOBJ
11+
from smithers.io.stlhandler import STLHandler
12+
1013

1114
class Propeller(object):
1215
"""
@@ -22,26 +25,32 @@ class Propeller(object):
2225

2326
def __init__(self, shaft, blade, n_blades):
2427
self.shaft_solid = shaft.generate_solid()
28+
2529
blade.apply_transformations(reflect=True)
26-
blade_solid = blade.generate_solid(max_deg=2,
27-
display=False,
28-
errors=None)
30+
blade_solid = blade.generate_solid(
31+
max_deg=2, display=False, errors=None
32+
)
2933
blades = []
3034
blades.append(blade_solid)
31-
for i in range(n_blades-1):
32-
blade.rotate(rad_angle=1.0*2.0*np.pi/float(n_blades))
33-
blade_solid = blade.generate_solid(max_deg=2, display=False, errors=None)
35+
for i in range(n_blades - 1):
36+
blade.rotate(rad_angle=1.0 * 2.0 * np.pi / float(n_blades))
37+
blade_solid = blade.generate_solid(
38+
max_deg=2, display=False, errors=None
39+
)
3440
blades.append(blade_solid)
3541
blades_combined = blades[0]
36-
for i in range(len(blades)-1):
37-
boolean_union = BRepAlgoAPI_Fuse(blades_combined, blades[i+1])
42+
for i in range(len(blades) - 1):
43+
boolean_union = BRepAlgoAPI_Fuse(blades_combined, blades[i + 1])
3844
boolean_union.Build()
3945
if not boolean_union.IsDone():
40-
raise RuntimeError('Unsuccessful assembling of blade')
46+
raise RuntimeError("Unsuccessful assembling of blade")
4147
blades_combined = boolean_union.Shape()
48+
self.blades_solid = blades_combined
49+
4250
boolean_union = BRepAlgoAPI_Fuse(self.shaft_solid, blades_combined)
4351
boolean_union.Build()
4452
result_compound = boolean_union.Shape()
53+
4554
sewer = BRepBuilderAPI_Sewing(1e-2)
4655
sewer.Add(result_compound)
4756
sewer.Perform()
@@ -51,9 +60,9 @@ def generate_iges(self, filename):
5160
"""
5261
Export the .iges CAD for the propeller with shaft.
5362
54-
:param string filename: path (with the file extension) where to store
63+
:param string filename: path (with the file extension) where to store
5564
the .iges CAD for the propeller and shaft
56-
:raises RuntimeError: if the solid assembling of blades is not
65+
:raises RuntimeError: if the solid assembling of blades is not
5766
completed successfully
5867
"""
5968
iges_writer = IGESControl_Writer()
@@ -64,13 +73,111 @@ def generate_stl(self, filename):
6473
"""
6574
Export the .stl CAD for the propeller with shaft.
6675
67-
:param string filename: path (with the file extension) where to store
76+
:param string filename: path (with the file extension) where to store
6877
the .stl CAD for the propeller and shaft
69-
:raises RuntimeError: if the solid assembling of blades is not
78+
:raises RuntimeError: if the solid assembling of blades is not
7079
completed successfully
7180
"""
7281
write_stl_file(self.sewed_full_body, filename)
7382

83+
def generate_obj(self, filename, region_selector="by_coords"):
84+
"""
85+
Export the .obj CAD for the propeller with shaft. The resulting
86+
file contains two regions: `propellerTip` and `propellerStem`, selected
87+
according to the criteria passed in the parameter `region_selector`.
88+
89+
:param string filename: path (with the file extension).
90+
:param string region_selector: Two selectors available:
91+
92+
* `by_coords`: We compute :math:`x`, the smallest X coordinate of
93+
the solid which represents the blades of the propeller. Then all
94+
the polygons (belonging to both blades and shaft) composed of
95+
points whose X coordinate is greater than :math:`x` are
96+
considered to be part of the region `propellerTip`. The rest
97+
belongs to `propellerStem`;
98+
* `blades_and_stem`: The two regions are simply given by the two
99+
solids which are used in :func:`__init__`.
100+
:raises RuntimeError: if the solid assembling of blades is not
101+
completed successfully
102+
"""
103+
104+
# we write the propeller to STL, then re-open it to obtain the points
105+
write_stl_file(self.shaft_solid, "/tmp/temp_shaft.stl")
106+
shaft = STLHandler.read("/tmp/temp_shaft.stl")
107+
write_stl_file(self.blades_solid, "/tmp/temp_blades.stl")
108+
blades = STLHandler.read("/tmp/temp_blades.stl")
109+
110+
obj_instance = WavefrontOBJ()
111+
112+
# add vertexes. first of all we check for duplicated vertexes
113+
all_vertices = np.concatenate(
114+
[shaft["points"], blades["points"]], axis=0
115+
)
116+
117+
# unique_mapping maps items in all_vertices to items in unique_vertices
118+
unique_vertices, unique_mapping = np.unique(
119+
all_vertices, return_inverse=True, axis=0
120+
)
121+
obj_instance.vertices = unique_vertices
122+
123+
def cells_to_np(cells):
124+
cells = np.asarray(cells)
125+
return unique_mapping[cells.flatten()].reshape(-1, cells.shape[1])
126+
127+
# append a list of cells to obj_instance.polygons, possibly with a
128+
# region name
129+
def append_cells(cells, region_name=None):
130+
if region_name is not None:
131+
obj_instance.regions_change_indexes.append(
132+
(
133+
np.asarray(obj_instance.polygons).shape[0],
134+
len(obj_instance.regions),
135+
)
136+
)
137+
obj_instance.regions.append(region_name)
138+
139+
if len(obj_instance.polygons) == 0:
140+
obj_instance.polygons = np.array(cells_to_np(cells))
141+
else:
142+
obj_instance.polygons = np.concatenate(
143+
[obj_instance.polygons, cells_to_np(cells)], axis=0
144+
)
145+
146+
shaft_cells = np.asarray(shaft["cells"])
147+
# the 0th point in blades if the last+1 point in shaft
148+
blades_cells = np.asarray(blades["cells"]) + len(shaft["points"])
149+
150+
if region_selector == "blades_and_stem":
151+
append_cells(blades_cells, region_name="propellerTip")
152+
append_cells(shaft_cells, region_name="propellerStem")
153+
elif region_selector == "by_coords":
154+
minimum_blades_x = np.min(blades["points"][:, 0])
155+
tip_boolean_array = shaft["points"][:, 0] >= minimum_blades_x
156+
shaft_cells_tip = np.all(
157+
tip_boolean_array[shaft_cells.flatten()].reshape(
158+
-1, shaft_cells.shape[1]
159+
),
160+
axis=1,
161+
)
162+
163+
append_cells(
164+
np.concatenate(
165+
[blades_cells, shaft_cells[shaft_cells_tip]], axis=0
166+
),
167+
region_name="propellerTip",
168+
)
169+
append_cells(
170+
shaft_cells[np.logical_not(shaft_cells_tip)],
171+
region_name="propellerStem",
172+
)
173+
else:
174+
raise ValueError("This selector is not supported at the moment")
175+
176+
# this is needed because indexes start at 1
177+
obj_instance.polygons += 1
178+
179+
ObjHandler.write(filename, obj_instance)
180+
74181
def display(self):
75182
"""
76183
Display the propeller with shaft.

Diff for: tests/test_propeller.py

+98-38
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,24 @@
44
import bladex.profiles as pr
55
import bladex.blade as bl
66
from bladex import NacaProfile, Shaft, Propeller
7+
from smithers.io.obj import ObjHandler
8+
from smithers.io.stlhandler import STLHandler
79

810

911
def create_sample_blade_NACApptc():
1012
sections = np.asarray([NacaProfile('5407') for i in range(13)])
11-
radii=np.array([0.034375, 0.0375, 0.04375, 0.05, 0.0625, 0.075, 0.0875,
13+
radii=np.array([0.034375, 0.0375, 0.04375, 0.05, 0.0625, 0.075, 0.0875,
1214
0.1, 0.10625, 0.1125, 0.11875, 0.121875, 0.125])
13-
chord_lengths = np.array([0.039, 0.045, 0.05625, 0.06542, 0.08125,
14-
0.09417, 0.10417, 0.10708, 0.10654, 0.10417,
15+
chord_lengths = np.array([0.039, 0.045, 0.05625, 0.06542, 0.08125,
16+
0.09417, 0.10417, 0.10708, 0.10654, 0.10417,
1517
0.09417, 0.07867, 0.025])
16-
pitch = np.array([0.35, 0.35, 0.36375, 0.37625, 0.3945, 0.405, 0.40875,
18+
pitch = np.array([0.35, 0.35, 0.36375, 0.37625, 0.3945, 0.405, 0.40875,
1719
0.4035, 0.3955, 0.38275, 0.3645, 0.35275, 0.33875])
18-
rake=np.array([0.0 ,0.0, 0.0005, 0.00125, 0.00335, 0.005875, 0.0075,
20+
rake=np.array([0.0 ,0.0, 0.0005, 0.00125, 0.00335, 0.005875, 0.0075,
1921
0.007375, 0.006625, 0.00545, 0.004033, 0.0033, 0.0025])
20-
skew_angles=np.array([6.6262795, 3.6262795, -1.188323, -4.4654502,
21-
-7.440779, -7.3840979, -5.0367916, -1.3257914,
22-
1.0856404, 4.1448947, 7.697235, 9.5368917,
22+
skew_angles=np.array([6.6262795, 3.6262795, -1.188323, -4.4654502,
23+
-7.440779, -7.3840979, -5.0367916, -1.3257914,
24+
1.0856404, 4.1448947, 7.697235, 9.5368917,
2325
11.397609])
2426
return bl.Blade(
2527
sections=sections,
@@ -38,54 +40,54 @@ class TestPropeller(TestCase):
3840
def test_sections_inheritance_NACApptc(self):
3941
prop= create_sample_blade_NACApptc()
4042
self.assertIsInstance(prop.sections[0], pr.NacaProfile)
41-
43+
4244
def test_radii_NACApptc(self):
4345
prop = create_sample_blade_NACApptc()
44-
np.testing.assert_equal(prop.radii, np.array([0.034375, 0.0375, 0.04375,
45-
0.05, 0.0625, 0.075,
46-
0.0875, 0.1, 0.10625,
47-
0.1125, 0.11875, 0.121875,
46+
np.testing.assert_equal(prop.radii, np.array([0.034375, 0.0375, 0.04375,
47+
0.05, 0.0625, 0.075,
48+
0.0875, 0.1, 0.10625,
49+
0.1125, 0.11875, 0.121875,
4850
0.125]))
4951

5052
def test_chord_NACApptc(self):
5153
prop = create_sample_blade_NACApptc()
52-
np.testing.assert_equal(prop.chord_lengths,np.array([0.039, 0.045,
53-
0.05625, 0.06542,
54-
0.08125, 0.09417,
55-
0.10417, 0.10708,
56-
0.10654, 0.10417,
57-
0.09417, 0.07867,
54+
np.testing.assert_equal(prop.chord_lengths,np.array([0.039, 0.045,
55+
0.05625, 0.06542,
56+
0.08125, 0.09417,
57+
0.10417, 0.10708,
58+
0.10654, 0.10417,
59+
0.09417, 0.07867,
5860
0.025]))
5961

6062
def test_pitch_NACApptc(self):
6163
prop = create_sample_blade_NACApptc()
62-
np.testing.assert_equal(prop.pitch, np.array([0.35, 0.35, 0.36375,
63-
0.37625, 0.3945, 0.405,
64-
0.40875, 0.4035, 0.3955,
65-
0.38275, 0.3645, 0.35275,
64+
np.testing.assert_equal(prop.pitch, np.array([0.35, 0.35, 0.36375,
65+
0.37625, 0.3945, 0.405,
66+
0.40875, 0.4035, 0.3955,
67+
0.38275, 0.3645, 0.35275,
6668
0.33875]))
6769

6870
def test_rake_NACApptc(self):
6971
prop = create_sample_blade_NACApptc()
70-
np.testing.assert_equal(prop.rake, np.array([0.0 ,0.0, 0.0005, 0.00125,
71-
0.00335, 0.005875, 0.0075,
72-
0.007375, 0.006625, 0.00545,
72+
np.testing.assert_equal(prop.rake, np.array([0.0 ,0.0, 0.0005, 0.00125,
73+
0.00335, 0.005875, 0.0075,
74+
0.007375, 0.006625, 0.00545,
7375
0.004033, 0.0033, 0.0025]))
7476

7577
def test_skew_NACApptc(self):
7678
prop = create_sample_blade_NACApptc()
77-
np.testing.assert_equal(prop.skew_angles, np.array([6.6262795,
78-
3.6262795,
79-
-1.188323,
80-
-4.4654502,
81-
-7.440779,
82-
-7.3840979,
83-
-5.0367916,
84-
-1.3257914,
85-
1.0856404,
86-
4.1448947,
87-
7.697235,
88-
9.5368917,
79+
np.testing.assert_equal(prop.skew_angles, np.array([6.6262795,
80+
3.6262795,
81+
-1.188323,
82+
-4.4654502,
83+
-7.440779,
84+
-7.3840979,
85+
-5.0367916,
86+
-1.3257914,
87+
1.0856404,
88+
4.1448947,
89+
7.697235,
90+
9.5368917,
8991
11.397609]))
9092

9193
def test_sections_array_different_length(self):
@@ -156,5 +158,63 @@ def test_generate_stl(self):
156158
self.assertTrue(os.path.isfile('tests/test_datasets/propeller_and_shaft.stl'))
157159
self.addCleanup(os.remove, 'tests/test_datasets/propeller_and_shaft.stl')
158160

161+
def test_generate_obj_by_coords(self):
162+
sh = Shaft("tests/test_datasets/shaft.iges")
163+
prop = create_sample_blade_NACApptc()
164+
prop = Propeller(sh, prop, 4)
165+
prop.generate_obj("tests/test_datasets/propeller_and_shaft.obj", region_selector='by_coords')
166+
167+
data = ObjHandler.read('tests/test_datasets/propeller_and_shaft.obj')
168+
assert data.regions == ['propellerTip','propellerStem']
169+
170+
# we want 0 to be the first index
171+
data.polygons = np.asarray(data.polygons) - 1
172+
173+
tip_poly = data.polygons[:data.regions_change_indexes[1][0]]
174+
stem_poly = data.polygons[data.regions_change_indexes[1][0]:]
175+
176+
blades_stl = STLHandler.read('/tmp/temp_blades.stl')
177+
shaft_stl = STLHandler.read('/tmp/temp_shaft.stl')
178+
179+
# same vertices
180+
all_vertices = np.concatenate(
181+
[shaft_stl["points"], blades_stl["points"]], axis=0
182+
)
183+
unique_vertices = np.unique(all_vertices, axis=0)
184+
np.testing.assert_almost_equal(data.vertices, unique_vertices, decimal=3)
185+
186+
blades_min_x = np.min(blades_stl['points'][:,0])
187+
188+
assert np.all(data.vertices[np.asarray(tip_poly).flatten()][:,0] >= blades_min_x)
189+
assert not any(np.all(data.vertices[np.asarray(stem_poly).flatten()][:,0].reshape(-1,data.polygons.shape[1]) >= blades_min_x, axis=1))
190+
191+
def test_generate_obj_blades_and_stem(self):
192+
sh = Shaft("tests/test_datasets/shaft.iges")
193+
prop = create_sample_blade_NACApptc()
194+
prop = Propeller(sh, prop, 4)
195+
prop.generate_obj("tests/test_datasets/propeller_and_shaft.obj", region_selector='blades_and_stem')
196+
197+
data = ObjHandler.read('tests/test_datasets/propeller_and_shaft.obj')
198+
assert data.regions == ['propellerTip','propellerStem']
199+
200+
tip_polygons = np.asarray(data.polygons[:data.regions_change_indexes[1][0]]) - 1
201+
stem_polygons = np.asarray(data.polygons[data.regions_change_indexes[1][0]:]) - 1
202+
203+
blades_stl = STLHandler.read('/tmp/temp_blades.stl')
204+
shaft_stl = STLHandler.read('/tmp/temp_shaft.stl')
205+
206+
# same vertices
207+
all_vertices = np.concatenate(
208+
[shaft_stl["points"], blades_stl["points"]], axis=0
209+
)
210+
211+
unique_vertices, indexing = np.unique(
212+
all_vertices, return_index=True, axis=0
213+
)
214+
np.testing.assert_almost_equal(data.vertices, unique_vertices, decimal=3)
215+
216+
assert np.all(indexing[stem_polygons.flatten()] < shaft_stl['points'].shape[0])
217+
assert np.all(indexing[tip_polygons.flatten()] >= shaft_stl['points'].shape[0])
218+
159219
def test_isdisplay(self):
160220
assert hasattr(Propeller, "display") == True

0 commit comments

Comments
 (0)