diff --git a/.github/workflows/testing_pr.yml b/.github/workflows/testing_pr.yml index 71025ea..8221e4f 100644 --- a/.github/workflows/testing_pr.yml +++ b/.github/workflows/testing_pr.yml @@ -13,36 +13,40 @@ jobs: fail-fast: false matrix: os: [windows-latest, macos-latest, ubuntu-latest] - python-version: [3.7, 3.8] + python-version: [3.8, 3.9] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Setup conda - uses: s-weigand/setup-conda@v1.1.1 + - name: Setup Conda + uses: conda-incubator/setup-miniconda@v3 with: - update-conda: true - python-version: ${{ matrix.python-version }} - conda-channels: anaconda, conda-forge - + channels: conda-forge, defaults + activate-environment: "" + - name: Install Python dependencies on Linux/MacOS + shell: bash -el {0} if: startsWith(matrix.os, 'windows') != true run: | - conda install --yes pythonocc-core=7.4.1 - python3 -m pip install --upgrade pip - python3 -m pip install smithers[vtk] - python3 -m pip install .[test] + conda create -n occ python=${{ matrix.python-version }} pythonocc-core + conda info + conda activate occ + conda info + python -m pip install --upgrade pip + python -m pip install smithers[vtk] + python -m pip install .[test] + python -c 'import OCC' - name: Install Python dependencies on Windows if: startsWith(matrix.os, 'windows') run: | - conda install --yes pythonocc-core=7.4.1 + conda install --yes pythonocc-core python -m pip install --upgrade pip python -m pip install smithers[vtk] python -m pip install .[test] @@ -52,5 +56,8 @@ jobs: run: python -m pytest - name: Test with pytest on Linux/MacOS + shell: bash -el {0} if: startsWith(matrix.os, 'windows') != true - run: python3 -m pytest + run: | + conda activate occ + python -m pytest diff --git a/bladex/blade.py b/bladex/blade.py index e516cb5..39f1a98 100644 --- a/bladex/blade.py +++ b/bladex/blade.py @@ -429,8 +429,6 @@ def scale(self, factor): self.blade_coordinates_down[i][1] = new_coord_matrix_down[1] self.blade_coordinates_down[i][2] = new_coord_matrix_down[2] - - def plot(self, elev=None, azim=None, ax=None, outfile=None): """ Plot the generated blade sections. @@ -1088,7 +1086,59 @@ def generate_stl_blade(self, filename): write_stl_file(self.sewed_full, filename) - def generate_iges_blade(self, filename): + def _generate_leading_edge_curves(self): + """ + Private method to generate curves that follow the leading edge of the blade + (top and bottom surfaces). + """ + self._import_occ_libs() + + # Extract points at leftmost 5% for upper and lower surfaces + upper_points = [] + lower_points = [] + + for i in range(self.n_sections): + min_x = np.min(self.sections[i].xdown_coordinates) + max_x = np.max(self.sections[i].xdown_coordinates) + delta_x = max_x - min_x + + target_x = min_x + 0.95 * delta_x + + idx = np.abs(self.sections[i].xdown_coordinates - target_x).argmin() + + # Create points for upper and lower curves + upper_points.append(gp_Pnt( + 1000 * self.blade_coordinates_up[i][0][idx], + 1000 * self.blade_coordinates_up[i][1][idx], + 1000 * self.blade_coordinates_up[i][2][idx] + )) + + lower_points.append(gp_Pnt( + 1000 * self.blade_coordinates_down[i][0][idx], + 1000 * self.blade_coordinates_down[i][1][idx], + 1000 * self.blade_coordinates_down[i][2][idx] + )) + + # Create arrays of points for interpolation + upper_array = TColgp_HArray1OfPnt(1, len(upper_points)) + lower_array = TColgp_HArray1OfPnt(1, len(lower_points)) + + for i, (up, low) in enumerate(zip(upper_points, lower_points)): + upper_array.SetValue(i + 1, up) + lower_array.SetValue(i + 1, low) + + # Create interpolated curves + upper_curve = GeomAPI_Interpolate(upper_array, False, 1e-9) + lower_curve = GeomAPI_Interpolate(lower_array, False, 1e-9) + + upper_curve.Perform() + lower_curve.Perform() + + # Convert to edges + self.upper_le_edge = BRepBuilderAPI_MakeEdge(upper_curve.Curve()).Edge() + self.lower_le_edge = BRepBuilderAPI_MakeEdge(lower_curve.Curve()).Edge() + + def generate_iges_blade(self, filename, include_le_curves=False): """ Generate and export the .IGES file for the entire blade. This method requires PythonOCC (7.4.0) to be installed. @@ -1099,11 +1149,20 @@ def generate_iges_blade(self, filename): self._generate_lower_face(max_deg=1) self._generate_root(max_deg=1) self._generate_tip(max_deg=1) + + if include_le_curves: + self._generate_leading_edge_curves() + iges_writer = IGESControl_Writer() iges_writer.AddShape(self.generated_upper_face) iges_writer.AddShape(self.generated_lower_face) iges_writer.AddShape(self.generated_root) iges_writer.AddShape(self.generated_tip) + + if include_le_curves: + iges_writer.AddShape(self.upper_le_edge) + iges_writer.AddShape(self.lower_le_edge) + iges_writer.Write(filename) @staticmethod @@ -1244,6 +1303,7 @@ def export_ppg(self, if i == len(hub_offsets) - 1: output_string += str("%.8e" % offset[0]) + ' ' + str( "%.8e" % hub_offsets[i][1]) + continue output_string += str("%.8e" % offset[0]) + ' ' + str( "%.8e" % offset[1]) + '\n' diff --git a/bladex/reversepropeller.py b/bladex/reversepropeller.py index f735130..83806c6 100644 --- a/bladex/reversepropeller.py +++ b/bladex/reversepropeller.py @@ -19,7 +19,7 @@ BRep_Tool_CurveOnSurface) import OCC.Core.TopoDS from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Fuse -from OCC.Core.BRepAlgo import BRepAlgo_Section +from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Section from OCC.Core.TopTools import TopTools_ListOfShape, TopTools_MapOfShape from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopAbs import TopAbs_VERTEX, TopAbs_EDGE, TopAbs_FACE, TopAbs_WIRE @@ -29,7 +29,7 @@ from OCC.Core.TopoDS import TopoDS_Shape from OCC.Core.TColgp import TColgp_HArray1OfPnt, TColgp_Array1OfPnt from OCC.Core.GeomAPI import GeomAPI_Interpolate, GeomAPI_IntCS, GeomAPI_ProjectPointOnSurf -from OCC.Core.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_HCurve +from OCC.Core.BRepAdaptor import BRepAdaptor_Curve from OCC.Core.GCPnts import GCPnts_AbscissaPoint from OCC.Core.BRep import BRep_Tool from OCC.Core.IntTools import IntTools_FClass2d @@ -38,7 +38,7 @@ from OCC.Core.TopoDS import topods, TopoDS_Edge, TopoDS_Compound from subprocess import call from OCC.Core.IntCurvesFace import IntCurvesFace_ShapeIntersector -from OCC.Core.Adaptor3d import Adaptor3d_Curve, Adaptor3d_HCurve +from OCC.Core.Adaptor3d import Adaptor3d_Curve from OCC.Core.Geom import Geom_Line from OCC.Display.SimpleGui import init_display from OCC.Core.BRepGProp import (brepgprop_LinearProperties, @@ -48,8 +48,8 @@ from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCylinder from OCC.Core.GeomLProp import GeomLProp_SLProps from OCC.Core.GCPnts import GCPnts_AbscissaPoint -from OCC.Core.BRepAdaptor import (BRepAdaptor_Curve, BRepAdaptor_HCurve, - BRepAdaptor_CompCurve, BRepAdaptor_HCompCurve) +from OCC.Core.BRepAdaptor import (BRepAdaptor_Curve, + BRepAdaptor_CompCurve) from OCC.Core.GCPnts import GCPnts_UniformDeflection from OCC.Core.GeomAPI import GeomAPI_PointsToBSpline from OCC.Core.GeomAbs import (GeomAbs_C0, GeomAbs_G1, GeomAbs_C1, GeomAbs_G2, diff --git a/tests/test_blade.py b/tests/test_blade.py index f24f8a2..ac85a33 100644 --- a/tests/test_blade.py +++ b/tests/test_blade.py @@ -835,3 +835,63 @@ def test_blade_str_method(self): string += '\nInduced rake from skew (in unit length)'\ ' for the sections = {}'.format(blade.induced_rake) assert blade.__str__() == string + + def test_blade_parameters_unchanged_after_transformations_and_rotation(self): + """Test that blade parameters remain unchanged after transformations and rotation.""" + # Create the original parameters + sections = np.asarray([NacaProfile(digits='0012') for i in range(10)]) + radii = np.arange(0.4, 1.31, 0.1) + chord_lengths = np.concatenate((np.arange(0.55, 1.1, 0.15), + np.arange(1.03, 0.9, -0.03), + np.array([0.3]))) + pitch = np.append(np.arange(3.0, 4., 0.2), np.arange(4.1, 3.2, -0.2)) + rake = np.append(np.arange(5e-3, 0.08, 1e-2), np.arange(0.075, 0.02, -3e-2)) + skew_angles = np.append(np.arange(-4., -9., -3.), np.arange(-7., 15., 3.)) + + # Store original section data + original_sections = [] + for section in sections: + original_sections.append({ + 'xup': section.xup_coordinates.copy(), + 'yup': section.yup_coordinates.copy(), + 'xdown': section.xdown_coordinates.copy(), + 'ydown': section.ydown_coordinates.copy() + }) + + # Build the blade + blade = bl.Blade( + sections=sections, + radii=radii, + chord_lengths=chord_lengths, + pitch=pitch, + rake=rake, + skew_angles=skew_angles) + + # Apply transformations and rotate 180 degrees around z-axis + blade.apply_transformations() + blade.rotate(deg_angle=180) + + # Verify parameters are unchanged + for i, section in enumerate(blade.sections): + np.testing.assert_array_equal( + section.xup_coordinates, + original_sections[i]['xup'] + ) + np.testing.assert_array_equal( + section.yup_coordinates, + original_sections[i]['yup'] + ) + np.testing.assert_array_equal( + section.xdown_coordinates, + original_sections[i]['xdown'] + ) + np.testing.assert_array_equal( + section.ydown_coordinates, + original_sections[i]['ydown'] + ) + + np.testing.assert_array_equal(blade.radii, radii) + np.testing.assert_array_equal(blade.chord_lengths, chord_lengths) + np.testing.assert_array_equal(blade.pitch, pitch) + np.testing.assert_array_equal(blade.rake, rake) + np.testing.assert_array_equal(blade.skew_angles, skew_angles)