├── assets
├── casing.png
├── shaft.png
└── stage_casing.png
├── turbodesigner
├── units.py
├── attachments
│ ├── __init__.py
│ └── firetree.py
├── airfoils
│ ├── __init__.py
│ ├── common.py
│ └── DCA.py
├── blade
│ ├── __init__.py
│ ├── metal_angle_methods
│ │ ├── __init__.py
│ │ └── johnsen_bullock.py
│ ├── vortex
│ │ ├── __init__.py
│ │ ├── free_vortex.py
│ │ └── common.py
│ ├── metal_angles.py
│ └── row.py
├── cad
│ ├── __init__.py
│ ├── blade.py
│ ├── common.py
│ ├── shaft.py
│ └── casing.py
├── __init__.py
├── visualizer.py
├── stage.py
├── turbomachinery.py
├── flow_station.py
└── exporter.py
├── MANIFEST.in
├── requirements.txt
├── pyproject.toml
├── tests
├── designs.py
├── compressor_blade_test.py
├── designs
│ ├── mark1.json
│ └── base_design.json
├── compressor_stage_test.py
├── compressor_vortex_test.py
├── compressor_turbomachinery_test.py
├── compressor_deviation_test.py
└── compressor_flow_station_test.py
├── .pylintrc
├── .vscode
├── launch.json
└── settings.json
├── .gitpod.yml
├── .devcontainer
├── devcontainer.json
└── Dockerfile
├── .github
└── workflows
│ └── python-publish.yml
├── LICENSE
├── setup.py
├── README.md
└── .gitignore
/assets/casing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenOrion/turbodesigner/HEAD/assets/casing.png
--------------------------------------------------------------------------------
/assets/shaft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenOrion/turbodesigner/HEAD/assets/shaft.png
--------------------------------------------------------------------------------
/turbodesigner/units.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 | MM = 1000
4 | DEG = 180/np.pi
5 | BAR = 1E-5
--------------------------------------------------------------------------------
/assets/stage_casing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenOrion/turbodesigner/HEAD/assets/stage_casing.png
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 | include README.md
3 | include requirements.txt
4 | recursive-include turbodesigner *.json
5 | recursive-include turbodesigner *.py
--------------------------------------------------------------------------------
/turbodesigner/attachments/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | TurboDesigner Attachments Module - Contains attachment design functionality
3 | """
4 |
5 | from turbodesigner.attachments.firetree import *
6 |
7 | __all__ = [
8 | "firetree",
9 | ]
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # Core dependencies
2 | kaleido
3 | plotly==5.11.0
4 | ipywidgets>=7.6
5 | jupyterlab
6 | matplotlib
7 | numpy
8 | dacite
9 | pandas
10 |
11 | # CAD dependencies
12 | cadquery
13 | git+https://github.com/gumyr/cq_warehouse.git
14 |
15 |
--------------------------------------------------------------------------------
/turbodesigner/airfoils/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | TurboDesigner Airfoils Module - Contains airfoil design functionality
3 | """
4 |
5 | from turbodesigner.airfoils.common import *
6 | from turbodesigner.airfoils.DCA import *
7 |
8 | __all__ = [
9 | "common",
10 | "DCA",
11 | ]
--------------------------------------------------------------------------------
/turbodesigner/blade/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | TurboDesigner Blade Module - Contains blade design functionality
3 | """
4 |
5 | from turbodesigner.blade.metal_angles import *
6 | from turbodesigner.blade.row import *
7 |
8 | __all__ = [
9 | "metal_angles",
10 | "row",
11 | ]
--------------------------------------------------------------------------------
/turbodesigner/blade/metal_angle_methods/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | TurboDesigner Metal Angle Methods Module - Contains methods for calculating metal angles
3 | """
4 |
5 | from turbodesigner.blade.metal_angle_methods.johnsen_bullock import *
6 |
7 | __all__ = [
8 | "johnsen_bullock",
9 | ]
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=42", "wheel"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [tool.black]
6 | line-length = 88
7 | target-version = ['py38']
8 | include = '\.pyi?$'
9 |
10 | [tool.isort]
11 | profile = "black"
12 | multi_line_output = 3
--------------------------------------------------------------------------------
/tests/designs.py:
--------------------------------------------------------------------------------
1 | import os
2 | from turbodesigner.turbomachinery import Turbomachinery
3 |
4 | TEST_DIR = os.path.dirname(os.path.abspath(__file__))
5 | base_design = Turbomachinery.from_file(f"{TEST_DIR}/designs/base_design.json")
6 | mark1 = Turbomachinery.from_file(f"{TEST_DIR}/designs/mark1.json")
7 |
--------------------------------------------------------------------------------
/turbodesigner/blade/vortex/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | TurboDesigner Vortex Module - Contains vortex design functionality
3 | """
4 |
5 | from turbodesigner.blade.vortex.common import *
6 | from turbodesigner.blade.vortex.free_vortex import *
7 |
8 | __all__ = [
9 | "common",
10 | "free_vortex",
11 | ]
--------------------------------------------------------------------------------
/.pylintrc:
--------------------------------------------------------------------------------
1 |
2 | [MESSAGES CONTROL]
3 | disable=import-error,
4 | invalid-name,
5 | non-ascii-name,
6 | missing-function-docstring,
7 | missing-module-docstring,
8 | missing-class-docstring,
9 | line-too-long,
10 | dangerous-default-value,
11 | line-too-long,
12 | unnecessary-lambda
--------------------------------------------------------------------------------
/turbodesigner/cad/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | TurboDesigner CAD Module - Contains CAD functionality
3 | """
4 |
5 | from turbodesigner.cad.blade import *
6 | from turbodesigner.cad.casing import *
7 | from turbodesigner.cad.common import *
8 | from turbodesigner.cad.shaft import *
9 |
10 | __all__ = [
11 | "blade",
12 | "casing",
13 | "common",
14 | "shaft",
15 | ]
--------------------------------------------------------------------------------
/turbodesigner/blade/vortex/free_vortex.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 | import numpy as np
3 | from turbodesigner.blade.vortex.common import Vortex
4 |
5 | class FreeVortex(Vortex):
6 | def ctheta(self, r: Union[float, np.ndarray], is_rotating: bool):
7 | mu = r/self.rm
8 | a = self.Um*(1-self.Rm)
9 | b = (1/2)*self.psi_m*self.Um
10 | sign = -1 if is_rotating else 1
11 | return (a/mu) + sign*(b/mu)
--------------------------------------------------------------------------------
/turbodesigner/airfoils/common.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | import numpy as np
3 |
4 | class AirfoilType(Enum):
5 | NACA65 = 1
6 | DCA = 2
7 | C4 = 2
8 |
9 | def get_staggered_coords(coords: np.ndarray, stagger_angle: float):
10 | x = coords[:, 0]
11 | y = coords[:, 1]
12 |
13 | return np.array([
14 | x*np.cos(stagger_angle) - y*np.sin(stagger_angle),
15 | x*np.sin(stagger_angle) + y*np.cos(stagger_angle),
16 | ]).T
17 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "Python: Current File",
9 | "type": "python",
10 | "request": "launch",
11 | "program": "${file}",
12 | "console": "integratedTerminal",
13 | "justMyCode": true
14 | }
15 | ]
16 | }
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | # This configuration file was automatically generated by Gitpod.
2 | # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
3 | # and commit this file to your remote git repository to share the goodness with others.
4 | image:
5 | file: .devcontainer/Dockerfile
6 |
7 | github:
8 | prebuilds:
9 | # enable for the master/default branch (defaults to true)
10 | master: true
11 | # enable for pull requests coming from this repo (defaults to true)
12 | pullRequests: false
13 | # add a "Review in Gitpod" button as a comment to pull requests (defaults to true)
14 | addComment: false
15 |
16 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Python 3",
3 | "build": {
4 | "dockerfile": "Dockerfile",
5 | "context": "..",
6 | "args": {
7 | // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6
8 | // Append -bullseye or -buster to pin to an OS version.
9 | // Use -bullseye variants on local on arm64/Apple Silicon.
10 | "VARIANT": "3.10-bullseye",
11 | // Options
12 | "NODE_VERSION": "lts/*"
13 | }
14 | },
15 | // Add the IDs of extensions you want installed when the container is created.
16 | "extensions": [
17 | "ms-python.python",
18 | "ms-python.vscode-pylance",
19 | "eamodio.gitlens"
20 | ],
21 | "remoteUser": "vscode"
22 | }
--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Python Package
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | deploy:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v3
11 | - name: Set up Python
12 | uses: actions/setup-python@v4
13 | with:
14 | python-version: '3.x'
15 | - name: Install dependencies
16 | run: |
17 | python -m pip install --upgrade pip
18 | pip install build
19 |
20 | - name: Build package
21 | run: python -m build
22 | - name: Publish package
23 | uses: pypa/gh-action-pypi-publish@release/v1
24 | with:
25 | password: ${{ secrets.PYPI_API_TOKEN }}
26 | packages_dir: dist/
--------------------------------------------------------------------------------
/tests/compressor_blade_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import numpy as np
3 | from tests.designs import base_design
4 |
5 | class CompressorDesignTest(unittest.TestCase):
6 | def test_flow_angles(self):
7 | alpha2 = base_design.stages[0].stator.flow_station.alpha
8 | beta1 = base_design.stages[0].rotor.flow_station.beta
9 | beta2 = base_design.stages[0].stator.flow_station.beta
10 |
11 | np.testing.assert_allclose(np.degrees(alpha2), np.array([33.89769494, 25.41478326, 20.1796603 ]))
12 | np.testing.assert_allclose(np.degrees(beta1), np.array([-50.78297859, -60.99475883, -67.28513168]))
13 | np.testing.assert_allclose(np.degrees(beta2), np.array([-30.28750731, -52.45117333, -62.59349512]))
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG VARIANT="3.10-bullseye"
2 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
3 |
4 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
5 | ARG NODE_VERSION="none"
6 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
7 |
8 | RUN apt-get update -y && \
9 | apt install -y libgl1-mesa-glx && \
10 | apt-get clean && \
11 | rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
12 |
13 |
14 | # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
15 | # COPY requirements.txt /tmp/pip-tmp/
16 | # RUN pip install -r /tmp/pip-tmp/requirements.txt && rm -rf /tmp/pip-tmp
17 |
--------------------------------------------------------------------------------
/turbodesigner/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | TurboDesigner - The open-source turbomachinery designer
3 | """
4 |
5 | __version__ = "0.1.0"
6 |
7 | # Import main modules for easier access
8 | from turbodesigner.flow_station import FlowStation
9 | from turbodesigner.stage import Stage
10 | from turbodesigner.turbomachinery import Turbomachinery
11 | from turbodesigner.units import MM, DEG, BAR
12 | from turbodesigner.visualizer import TurbomachineryVisualizer
13 | from turbodesigner.exporter import TurbomachineryExporter
14 |
15 | # Make these modules available at the package level
16 | __all__ = [
17 | "FlowStation",
18 | "Stage",
19 | "Turbomachinery",
20 | "MM",
21 | "DEG",
22 | "BAR",
23 | "TurbomachineryVisualizer",
24 | "TurbomachineryExporter",
25 | ]
--------------------------------------------------------------------------------
/tests/designs/mark1.json:
--------------------------------------------------------------------------------
1 | {
2 | "gamma": 1.4,
3 | "cx": 136,
4 | "N": 10000,
5 | "Rs": 287,
6 | "mdot": 4.374433141191892,
7 | "PR": 3,
8 | "Pt": 101000,
9 | "Tt": 288,
10 | "eta_isen": 0.87848151,
11 | "N_stg": 5,
12 | "Delta_T0_stg": "equal",
13 | "R_stg": [0.5, 0.5, 0.5, 0.5, 0.5],
14 | "B_in": 0.0,
15 | "B_out": 0.0,
16 | "ht": 0.5,
17 | "N_stream": 9,
18 | "AR": {"rotor": 3.0, "stator": 3.25},
19 | "sc": {"rotor": 1.0, "stator": 1.0},
20 | "tbc": {"rotor": 0.1, "stator": 0.1},
21 | "rgc": 0.25,
22 | "sgc": 0.5
23 | }
24 |
25 |
--------------------------------------------------------------------------------
/turbodesigner/blade/vortex/common.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from functools import cached_property
3 | from typing import Union
4 | import numpy as np
5 |
6 | @dataclass
7 | class Vortex:
8 | Um: float
9 | "mean blade velocity (m/s)"
10 |
11 | Vm: float
12 | "meridional flow velocity (m/s)"
13 |
14 | Rm: float
15 | "mean reaction rate (dimensionless)"
16 |
17 | psi_m: float
18 | "mean loading coefficient (dimensionless)"
19 |
20 | rm: float
21 | "mean radius (m)"
22 |
23 | def __post_init__(self):
24 | self.phi_m = self.Vm/self.Um
25 |
26 | def ctheta(self, r: Union[float, np.ndarray], is_rotating: bool):
27 | "absolute tangential velocity (m/s)"
28 | return np.nan
29 |
30 | def alpha(self, r: Union[float, np.ndarray], is_rotating: bool):
31 | "absolute flow angle (rad)"
32 | return np.arctan(self.ctheta(r, is_rotating)/self.Vm)
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Open Orion LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/turbodesigner/blade/metal_angles.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from typing import Union
3 | import numpy as np
4 | from turbodesigner.units import DEG
5 |
6 | @dataclass
7 | class MetalAngles:
8 |
9 | beta1: Union[float, np.ndarray]
10 | "blade inlet flow angle (rad)"
11 |
12 | beta2: Union[float, np.ndarray]
13 | "blade outlet flow angle (rad)"
14 |
15 | i: Union[float, np.ndarray]
16 | "blade incidence (rad)"
17 |
18 | delta: Union[float, np.ndarray]
19 | "blade deviation (rad)"
20 |
21 | kappa1: np.ndarray = field(init=False)
22 | "inlet metal angle (rad)"
23 |
24 | kappa2: np.ndarray = field(init=False)
25 | "outlet metal angle (rad)"
26 |
27 | theta: np.ndarray = field(init=False)
28 | "camber angle (rad)"
29 |
30 | xi: np.ndarray = field(init=False)
31 | "stagger angle (rad)"
32 |
33 | def __post_init__(self):
34 | self.kappa1 = np.asarray(self.beta1 - self.i)
35 | self.kappa2 = np.asarray(self.beta2 - self.delta)
36 | self.theta = np.asarray(self.kappa1-self.kappa2)
37 | self.xi = np.asarray((self.kappa1 + self.kappa2)/2)
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.testing.unittestArgs": [
3 | "-v",
4 | "-s",
5 | "./tests",
6 | "-p",
7 | "*_test.py"
8 | ],
9 | "python.testing.pytestEnabled": false,
10 | "python.testing.unittestEnabled": true,
11 | "python.linting.enabled": false,
12 | "python.defaultInterpreterPath": "/opt/conda/bin/python",
13 | "python.linting.pylintEnabled": true,
14 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
15 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black",
16 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
17 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
18 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
19 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
20 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
21 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
22 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
23 | "jupyter.notebookFileRoot": "${workspaceFolder}",
24 | "python.formatting.autopep8Args": [
25 | "--max-line-length=1000"
26 | ],
27 | "python.linting.pylintArgs": [
28 | "--extension-pkg-whitelist=numpy"
29 | ],
30 | "python.analysis.typeCheckingMode": "basic",
31 | }
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | from setuptools import setup, find_packages
3 |
4 | # Read the contents of README.md
5 | with open("README.md", "r", encoding="utf-8") as fh:
6 | long_description = fh.read()
7 |
8 | # Read requirements.txt
9 | with open("requirements.txt", "r", encoding="utf-8") as f:
10 | requirements = []
11 | for line in f:
12 | line = line.strip()
13 | # Skip comments and empty lines
14 | if not line or line.startswith("#"):
15 | continue
16 | # Skip git+ dependencies for PyPI compatibility
17 | if line.startswith("git+"):
18 | # Git dependencies aren't directly supported by PyPI
19 | # They'll be handled through extras_require
20 | continue
21 |
22 | requirements.append(line)
23 |
24 | setup(
25 | name="turbodesigner",
26 | version="1.0.0",
27 | author="Open Orion, Inc.",
28 | description="An open-source turbomachinery designer",
29 | long_description=long_description,
30 | long_description_content_type="text/markdown",
31 | url="https://github.com/OpenOrion/turbodesigner",
32 | packages=find_packages(),
33 | classifiers=[
34 | "Programming Language :: Python :: 3",
35 | "License :: OSI Approved :: MIT License",
36 | "Operating System :: OS Independent",
37 | "Topic :: Scientific/Engineering :: Physics",
38 | "Topic :: Scientific/Engineering :: Visualization",
39 | ],
40 | python_requires=">=3.8",
41 | install_requires=requirements,
42 | include_package_data=True,
43 | package_data={
44 | "turbodesigner": ["**/*.json"],
45 | },
46 | )
--------------------------------------------------------------------------------
/tests/designs/base_design.json:
--------------------------------------------------------------------------------
1 | {
2 | "gamma": 1.4,
3 | "cx": 150,
4 | "N": 15000,
5 | "Rs": 287,
6 | "mdot": 20,
7 | "PR": 4.15,
8 | "Pt": 101000,
9 | "Tt": 288,
10 | "eta_isen": 0.87848151,
11 | "N_stg": 7,
12 | "Delta_T0_stg": [20, 25, 25, 25, 25, 25, 20],
13 | "R_stg": [0.874, 0.7, 0.5, 0.5, 0.5, 0.5, 0.5],
14 | "B_in": 0,
15 | "B_out": 0,
16 | "ht": 0.5,
17 | "N_stream": 3,
18 | "AR": [
19 | {"rotor": 0.5, "stator": 0.5},
20 | {"rotor": 0.5, "stator": 0.5},
21 | {"rotor": 0.5, "stator": 0.5},
22 | {"rotor": 0.5, "stator": 0.5},
23 | {"rotor": 0.5, "stator": 0.5},
24 | {"rotor": 0.5, "stator": 0.5},
25 | {"rotor": 0.5, "stator": 0.5}
26 | ],
27 | "sc": [
28 | {"rotor": 1.0, "stator": 1.0},
29 | {"rotor": 1.0, "stator": 1.0},
30 | {"rotor": 1.0, "stator": 1.0},
31 | {"rotor": 1.0, "stator": 1.0},
32 | {"rotor": 1.0, "stator": 1.0},
33 | {"rotor": 1.0, "stator": 1.0},
34 | {"rotor": 1.0, "stator": 1.0}
35 | ],
36 | "tbc": [
37 | {"rotor": 0.1, "stator": 0.1},
38 | {"rotor": 0.1, "stator": 0.1},
39 | {"rotor": 0.1, "stator": 0.1},
40 | {"rotor": 0.1, "stator": 0.1},
41 | {"rotor": 0.1, "stator": 0.1},
42 | {"rotor": 0.1, "stator": 0.1},
43 | {"rotor": 0.1, "stator": 0.1}
44 | ],
45 | "rgc": 0.25,
46 | "sgc": 0.5
47 |
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/tests/compressor_stage_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import numpy as np
3 | from tests.designs import base_design
4 |
5 |
6 | class CompressorStageTest(unittest.TestCase):
7 | def test_first_stage_base_design(self):
8 | next_stage = base_design.stages[1]
9 | np.testing.assert_almost_equal(next_stage.inlet_flow_station.Tt, 308)
10 | np.testing.assert_almost_equal(next_stage.mid_flow_station.Tt, 333)
11 | np.testing.assert_almost_equal(next_stage.inlet_flow_station.T, 296.265, 3)
12 | np.testing.assert_almost_equal(next_stage.mid_flow_station.T, 313.76513, 5)
13 |
14 | np.testing.assert_almost_equal(next_stage.inlet_flow_station.Pt, 124787.12942, 5)
15 | np.testing.assert_almost_equal(next_stage.mid_flow_station.Pt, 159563.80095, 5)
16 | np.testing.assert_almost_equal(next_stage.inlet_flow_station.P, 108924.147, 3)
17 | np.testing.assert_almost_equal(next_stage.mid_flow_station.P, 129567.45266, 5)
18 | np.testing.assert_almost_equal(next_stage.inlet_flow_station.rho, 1.281, 3)
19 | np.testing.assert_almost_equal(next_stage.mid_flow_station.rho, 1.43883, 5)
20 |
21 | np.testing.assert_almost_equal(next_stage.inlet_flow_station.inner_radius, 0.121, 3)
22 | np.testing.assert_almost_equal(next_stage.inlet_flow_station.outer_radius, 0.218, 3)
23 | np.testing.assert_almost_equal(next_stage.inlet_flow_station.radius, 0.1696031)
24 | np.testing.assert_almost_equal(next_stage.mid_flow_station.inner_radius, 0.12612, 5)
25 | np.testing.assert_almost_equal(next_stage.mid_flow_station.outer_radius, 0.21308, 5)
26 | np.testing.assert_almost_equal(next_stage.mid_flow_station.radius, 0.1696031)
27 |
28 |
29 | if __name__ == '__main__':
30 | unittest.main()
31 |
--------------------------------------------------------------------------------
/tests/compressor_vortex_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import numpy as np
3 | from turbodesigner.flow_station import FlowStation
4 | from turbodesigner.blade.vortex.free_vortex import FreeVortex
5 |
6 |
7 | class VelocityTriangleTest(unittest.TestCase):
8 | def test_free_vortex_angles(self):
9 | N = 6000 # RPM
10 | rm = 0.475 # m
11 | Um = (1/30)*np.pi*N*rm # m/s
12 | cx = 136 # m/s
13 | Rm = 1-0.5*(0.5/0.475)**2
14 | ctheta1_m = 78.6 * (0.5/.475)
15 | ctheta2_m = 235.6 * (0.5/.475)
16 | psi_m = (ctheta2_m - ctheta1_m) / Um
17 | vortex = FreeVortex(
18 | Um=Um, # m/s
19 | Vm=cx, # m/s
20 | Rm=Rm, # dimensionless
21 | psi_m=psi_m, # dimensionless
22 | rm=rm # m
23 | )
24 |
25 | ctheta1_hub = vortex.ctheta(r=0.45, is_rotating=True)
26 | ctheta2_hub = vortex.ctheta(r=0.45, is_rotating=False)
27 | alpha1_hub = vortex.alpha(r=0.45, is_rotating=True)
28 | alpha2_hub = vortex.alpha(r=0.45, is_rotating=False)
29 |
30 | ctheta1_tip = vortex.ctheta(r=0.5, is_rotating=True)
31 | ctheta2_tip = vortex.ctheta(r=0.5, is_rotating=False)
32 | alpha1_tip = vortex.alpha(r=0.5, is_rotating=True)
33 | alpha2_tip = vortex.alpha(r=0.5, is_rotating=False)
34 |
35 | # Hub
36 | np.testing.assert_almost_equal(ctheta1_hub, 87.31, 2)
37 | np.testing.assert_almost_equal(ctheta2_hub, 261.755, 3)
38 | np.testing.assert_almost_equal(np.degrees(alpha1_hub), 32.70, 2)
39 | np.testing.assert_almost_equal(np.degrees(alpha2_hub), 62.54, 2)
40 |
41 | # Tip
42 | np.testing.assert_almost_equal(ctheta1_tip, 78.57, 2)
43 | np.testing.assert_almost_equal(ctheta2_tip, 235.58, 2)
44 | np.testing.assert_almost_equal(np.degrees(alpha1_tip), 30.02, 2)
45 | np.testing.assert_almost_equal(np.degrees(alpha2_tip), 60.00, 2)
46 |
47 |
48 | if __name__ == '__main__':
49 | unittest.main()
50 |
--------------------------------------------------------------------------------
/tests/compressor_turbomachinery_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import numpy as np
3 | from tests.designs import base_design
4 | from turbodesigner.flow_station import FlowStation
5 | from turbodesigner.stage import Stage
6 |
7 |
8 | class CompressorDesignTest(unittest.TestCase):
9 | def test_flow_station_base_design(self):
10 | np.testing.assert_almost_equal(base_design.outlet_flow_station.Pt, 419150)
11 | np.testing.assert_almost_equal(base_design.inlet_flow_station.P, 87908.56, 2)
12 | np.testing.assert_almost_equal(base_design.outlet_flow_station.P, 383948.29, 2)
13 | np.testing.assert_almost_equal(base_design.inlet_flow_station.T, 276.80039821)
14 | np.testing.assert_almost_equal(base_design.outlet_flow_station.T, 441.27919465)
15 | np.testing.assert_almost_equal(base_design.inlet_flow_station.rho, 1.10657931)
16 | np.testing.assert_almost_equal(base_design.outlet_flow_station.rho, 3.03163833)
17 | np.testing.assert_almost_equal(base_design.inlet_flow_station.A_flow, 0.12049144)
18 | np.testing.assert_almost_equal(base_design.inlet_flow_station.A_phys, 0.12049144)
19 | np.testing.assert_almost_equal(base_design.outlet_flow_station.A_flow, 0.04398062)
20 | np.testing.assert_almost_equal(base_design.outlet_flow_station.A_phys, 0.044, 3)
21 | np.testing.assert_almost_equal(base_design.outlet_flow_station.Tt, 452.47879644)
22 | np.testing.assert_almost_equal(base_design.Delta_T0, 164.47879644)
23 | np.testing.assert_almost_equal(base_design.inlet_flow_station.inner_radius, 0.11307, 5)
24 | np.testing.assert_almost_equal(base_design.outlet_flow_station.inner_radius, 0.14897, 5)
25 | np.testing.assert_almost_equal(base_design.inlet_flow_station.outer_radius, 0.22614, 5)
26 | np.testing.assert_almost_equal(base_design.outlet_flow_station.outer_radius, 0.19024, 5)
27 | np.testing.assert_almost_equal(base_design.inlet_flow_station.radius, 0.16960, 5)
28 | np.testing.assert_almost_equal(base_design.outlet_flow_station.radius, 0.16960, 5)
29 | np.testing.assert_almost_equal(base_design.inlet_flow_station.N, 15000)
30 |
31 |
32 | if __name__ == '__main__':
33 | unittest.main()
34 |
--------------------------------------------------------------------------------
/tests/compressor_deviation_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | from turbodesigner.blade.deviation.johnsen_bullock import JohnsenBullockBladeDeviation, AirfoilType
3 | import numpy as np
4 |
5 |
6 | class AungierDeviationTest(unittest.TestCase):
7 |
8 | def test_aungier_deviation(self):
9 | deviation = JohnsenBullockBladeDeviation(
10 | beta1=np.radians(70), # rad
11 | beta2=np.radians(20), # rad
12 | sigma=2.0, # dimensionless
13 | tbc=0.1, # dimensionless
14 | airfoil_type=AirfoilType.NACA65
15 | )
16 |
17 | metal_angles = deviation.get_metal_angles(100)
18 |
19 |
20 | np.testing.assert_almost_equal(np.degrees(metal_angles.kappa1), 73.9657, 4)
21 | np.testing.assert_almost_equal(np.degrees(metal_angles.kappa2), -0.4597, 4)
22 |
23 |
24 |
25 | def test_aungier_zero_camber_sigma_2(self):
26 | deviation = JohnsenBullockBladeDeviation(
27 | beta1=np.radians(70), # rad
28 | beta2=np.radians(70), # rad
29 | sigma=2.0, # dimensionless
30 | tbc=0.1, # dimensionless
31 | airfoil_type=AirfoilType.NACA65
32 | )
33 |
34 | metal_angles = deviation.get_metal_angles(1)
35 | np.testing.assert_almost_equal(np.degrees(metal_angles.i), 10.1975, 4)
36 | np.testing.assert_almost_equal(np.degrees(metal_angles.delta), 4.7296, 4)
37 |
38 | def test_aungier_zero_camber_sigma_1(self):
39 | deviation = JohnsenBullockBladeDeviation(
40 | beta1=np.radians(70), # rad
41 | beta2=np.radians(70), # rad
42 | sigma=1.0, # dimensionless
43 | tbc=0.1, # dimensionless
44 | airfoil_type=AirfoilType.NACA65
45 | )
46 |
47 | metal_angles = deviation.get_metal_angles(1)
48 | np.testing.assert_almost_equal(np.degrees(metal_angles.i), 5.0897, 4)
49 | np.testing.assert_almost_equal(np.degrees(metal_angles.delta), 2.5691, 4)
50 |
51 |
52 |
53 | if __name__ == '__main__':
54 | unittest.main()
55 |
--------------------------------------------------------------------------------
/turbodesigner/visualizer.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from turbodesigner.turbomachinery import Turbomachinery
3 | import plotly.graph_objects as go
4 | from IPython.display import Image, display
5 |
6 |
7 | class TurbomachineryVisualizer:
8 | @staticmethod
9 | def visualize_annulus(turbomachinery: Turbomachinery, is_interactive=False):
10 | rotors = [
11 | np.array([
12 | [(stage.stage_number-1.), stage.rotor.rt],
13 | [(stage.stage_number-1.)+0.5, stage.stator.rt],
14 | [(stage.stage_number-1.)+0.5, stage.stator.rh],
15 | [(stage.stage_number-1.), stage.rotor.rh],
16 | [(stage.stage_number-1.), stage.rotor.rt],
17 | ])
18 | for stage in turbomachinery.stages
19 | ]
20 |
21 | stators = [
22 | np.array([
23 | [(stage.stage_number-1.)+0.5, stage.stator.rt],
24 | [stage.stage_number, (stage.next_stage.rotor if stage.next_stage else stage.stator).rt],
25 | [stage.stage_number, (stage.next_stage.rotor if stage.next_stage else stage.stator).rh],
26 | [(stage.stage_number-1.)+0.5, stage.stator.rh],
27 | [(stage.stage_number-1.)+0.5, stage.stator.rt],
28 | ])
29 | for stage in turbomachinery.stages
30 | ]
31 |
32 |
33 | fig = go.Figure(
34 | layout=go.Layout(
35 | title=go.layout.Title(text="Annulus"),
36 | )
37 | )
38 | for (i, rotor) in enumerate(rotors):
39 | fig.add_trace(go.Scatter(
40 | x=rotor[:, 0],
41 | y=rotor[:, 1],
42 | fill="toself",
43 | fillcolor="red",
44 | line_color="red",
45 | legendgroup="rotor",
46 | legendgrouptitle_text="Rotor",
47 | name=f"Rotor {i+1}"
48 | ))
49 |
50 | for (i, stator) in enumerate(stators):
51 | fig.add_trace(go.Scatter(
52 | x=stator[:, 0],
53 | y=stator[:, 1],
54 | fill="toself",
55 | fillcolor="blue",
56 | line_color="blue",
57 | legendgroup="stator",
58 | legendgrouptitle_text="Stator",
59 | name=f"Stator {i+1}"
60 | ))
61 |
62 |
63 |
64 | if is_interactive:
65 | fig.show()
66 | else:
67 | image = Image(fig.to_image(format="png", width=800, height=500, scale=2))
68 | display(image)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TurboDesigner
2 | the open-source turbomachinery designer
3 |
4 |
5 | 
6 |
Axial Shaft
7 |
8 | 
9 | Axial Stage Casing
10 |
11 | # About
12 | Turbodesigner is a tool that given [parameters](https://github.com/Turbodesigner/turbodesigner/blob/main/tests/designs/mark1.json) such as pressure ratio and mass flow rate can generate designs using mean-line design, blade flow analysis, and at the end generate a CAD model that can be exported to STL and STEP files.
13 |
14 | Currently this generates axial compressors and with further tweaks axial turbopumps for liquid rocket engines
15 |
16 | # Assumptions
17 | To avoid feature creep or due to lack of development the following assumption are made:
18 | * `Turbomachinery` is an axial compressor (will suport more in later versions)
19 | * `FlowStation` assumes an Ideal Gas
20 | * Mean line is constant and is based on hub to tip ratio
21 | * Blade calculations are base on the `mean (rm)` station
22 | * Stagger angles are generated with `FreeVortex` (will support more in the future)
23 | * Blade `airfoil` is only a Double Circular Arc at the moment since other geometries haven't been implemented
24 | * `incidence (i)` and `deviation (delta)` values are defaulted 0 (will get Johnson Method working, but at the moment it is disabled)
25 |
26 | There are plans later to make the classes that make calculations
27 | extendable for certain circumstances
28 |
29 |
30 | ## Installation
31 | ```
32 | # For now git+ is the only way to get cq_warehouse
33 | pip install git+https://github.com/gumyr/cq_warehouse.git
34 |
35 | pip install turbodesigner
36 |
37 | # Optional: To display but there is the basic Jupyter Viewer and others
38 | pip install jupyter-cadquery==3.4.0 cadquery-massembly==1.0.0rc0 # for viewing in Jupyter
39 | ```
40 |
41 |
42 | # Setup
43 |
44 | ## Open in Gitpod
45 | [](https://gitpod.io/github.com/`/turbodesigner)
46 |
47 | or
48 |
49 | ```
50 | git clone https://github.com/Turbodesigner/turbodesigner.git
51 | cd turbodesigner
52 | pip install git+https://github.com/gumyr/cq_warehouse.git
53 | pip install jupyter-cadquery==3.4.0 cadquery-massembly==1.0.0rc0 # for viewing in Jupyter
54 | ```
55 |
56 | # Help Wanted
57 | Right now there are some items such as verifying calculations, CFD analysis, and adding additional logic for blade analysis. View [Projects](https://github.com/orgs/Turbodesigner/projects/1) tab for specific asks. Please join the [Discord](https://discord.gg/H7qRauGkQ6) for project communications and collaboration. Please consider donating to the [Patreon](https://www.patreon.com/openorion) to support future work on this project.
58 |
--------------------------------------------------------------------------------
/tests/compressor_flow_station_test.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import numpy as np
3 | from turbodesigner.flow_station import FlowStation
4 | from turbodesigner.stage import Stage
5 |
6 |
7 | class CompressorFlowStationTest(unittest.TestCase):
8 | def test_stage_flow_station(self):
9 | inlet_flow_station = FlowStation(
10 | gamma=1.4, # dimensionless
11 | Rs=287, # J/kg/K
12 | Tt=450, # K
13 | Pt=2.80E5, # Pa
14 | mdot=2.2, # kg/s
15 | Vm=263.95, # m/s
16 | alpha=0, # rad
17 | N=36000, # rpm
18 | radius=0.08, # m
19 | )
20 |
21 | np.testing.assert_almost_equal(inlet_flow_station.beta, np.radians(-48.808), 3)
22 | np.testing.assert_almost_equal(inlet_flow_station.U, 301.593, 3)
23 | np.testing.assert_almost_equal(inlet_flow_station.V, 263.95, 3)
24 | np.testing.assert_almost_equal(inlet_flow_station.Vcr, 388.169, 3)
25 | np.testing.assert_almost_equal(inlet_flow_station.Ttr, 495.275, 3)
26 | np.testing.assert_almost_equal(inlet_flow_station.Ptr, 391631.736, 3)
27 | np.testing.assert_almost_equal(inlet_flow_station.Vtheta, 0.00, 3)
28 |
29 | mid_flow_station = FlowStation(
30 | gamma=1.4, # dimensionless
31 | Rs=287, # J/kg/K
32 | Tt=504.6, # K
33 | Pt=3.60E5, # Pa
34 | mdot=2.2, # kg/s
35 | Vm=263.95, # m/s
36 | alpha=0.593, # rad
37 | N=36000, # rpm
38 | radius=0.08, # m
39 | )
40 |
41 | np.testing.assert_almost_equal(mid_flow_station.Wtheta, -123.715, 3)
42 | np.testing.assert_almost_equal(mid_flow_station.Ttr, 496.469, 3)
43 | np.testing.assert_almost_equal(mid_flow_station.Ptr,340102.039, 3)
44 |
45 |
46 | def test_velocity_triangle(self):
47 | inlet_flow_station = FlowStation(
48 | Vm=150, # m/s
49 | N=15000, # RPM
50 | radius=0.1696031, # m
51 | alpha=0, # rad
52 | )
53 |
54 | outlet_flow_station = FlowStation(
55 | Vm=150, # m/s
56 | N=15000, # RPM
57 | radius=0.1696031, # m
58 | alpha=np.radians(26.69005971), # rad
59 | )
60 |
61 | # Inlet Flow Station
62 | np.testing.assert_almost_equal(np.degrees(inlet_flow_station.alpha), 0.0)
63 | np.testing.assert_almost_equal(np.degrees(inlet_flow_station.beta), -60.61884197)
64 | np.testing.assert_almost_equal(inlet_flow_station.Vm, 150.0)
65 | np.testing.assert_almost_equal(inlet_flow_station.Vtheta, 0.0)
66 | np.testing.assert_almost_equal(inlet_flow_station.V, 150.0)
67 | np.testing.assert_almost_equal(inlet_flow_station.U, 266.41192649)
68 | np.testing.assert_almost_equal(inlet_flow_station.Wtheta, -266.41192649)
69 | np.testing.assert_almost_equal(inlet_flow_station.W, 305.73732938)
70 |
71 | # Outlet Flow Station
72 | np.testing.assert_almost_equal(np.degrees(outlet_flow_station.alpha), 26.69005971)
73 | np.testing.assert_almost_equal(np.degrees(outlet_flow_station.beta), -51.85637225)
74 | np.testing.assert_almost_equal(outlet_flow_station.Vm, 150.0)
75 | np.testing.assert_almost_equal(outlet_flow_station.Vtheta, 75.40953689)
76 | np.testing.assert_almost_equal(outlet_flow_station.V, 167.88864838)
77 | np.testing.assert_almost_equal(outlet_flow_station.Wtheta, -191.0023896)
78 | np.testing.assert_almost_equal(outlet_flow_station.W, 242.86192133)
79 |
80 |
81 | if __name__ == '__main__':
82 | unittest.main()
83 |
--------------------------------------------------------------------------------
/turbodesigner/blade/metal_angle_methods/johnsen_bullock.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from functools import cached_property
3 | from typing import Union
4 | import numpy as np
5 | from turbodesigner.airfoils import AirfoilType
6 | from turbodesigner.blade.metal_angles import MetalAngles
7 |
8 |
9 | @dataclass
10 | class MetalAngleOffset:
11 | i: Union[float, np.ndarray]
12 | "blade incidence (rad)"
13 |
14 | delta: Union[float, np.ndarray]
15 | "blade deviation (rad)"
16 |
17 |
18 | @dataclass
19 | class JohnsenBullockMetalAngleMethod:
20 | "Johnsen and Bullock 1965"
21 |
22 | beta1: Union[float, np.ndarray]
23 | "inlet flow angle (rad)"
24 |
25 | beta2: Union[float, np.ndarray]
26 | "outlet flow angle (rad)"
27 |
28 | sigma: float
29 | "spacing between blades (m)"
30 |
31 | tbc: float
32 | "max thickness to chord (dimensionless)"
33 |
34 | airfoil_type: AirfoilType
35 | "airfoil type (AirfoilType)"
36 |
37 | def __post_init__(self):
38 | self.beta1_deg = np.abs(np.degrees(self.beta1))
39 | self.beta2_deg = np.abs(np.degrees(self.beta2))
40 |
41 | @cached_property
42 | def Ksh(self):
43 | "blade shape paramter (dimensionless)"
44 | if self.airfoil_type == AirfoilType.NACA65:
45 | return 1.0
46 | elif self.airfoil_type == AirfoilType.DCA:
47 | return 0.7
48 | elif self.airfoil_type == AirfoilType.C4:
49 | return 1.1
50 | else:
51 | return 1.0 # Default case
52 |
53 | @cached_property
54 | def n(self):
55 | "slope factor (dimensionless)"
56 | return 0.025*self.sigma - ((1/90)*self.beta1_deg)**(1.2*self.sigma + 1)/(0.43*self.sigma + 1.5) - 0.06
57 |
58 | @cached_property
59 | def kti(self):
60 | "design incidence angle correction factor (dimensionless)"
61 | q = 0.28/(self.tbc**0.3 + 0.1)
62 | return (10*self.tbc)**q
63 |
64 | @cached_property
65 | def m(self):
66 | "deviation slope factor (dimensionless)"
67 | x = (1/100)*self.beta1_deg
68 |
69 | if self.airfoil_type == AirfoilType.NACA65:
70 | m1 = 0.333*x**2 - 0.0333*x + 0.17
71 | else:
72 | m1 = 0.316*x**3 - 0.132*x**2 + 0.074*x + 0.249
73 |
74 | b = -0.85*x**3 - 0.17*x + 0.9625
75 | return self.sigma**(-b)*m1
76 |
77 | @cached_property
78 | def Ktdelta(self):
79 | "design deviation angle correction factor (dimensionless)"
80 | return 37.5*self.tbc**2 + 6.25*self.tbc
81 |
82 | @cached_property
83 | def delta_star_0_10(self):
84 | "nominal deviation angle theta=0, tbc=0.10 (deg)"
85 | return 0.01*self.beta1_deg*self.sigma + ((0.74*self.sigma**1.9) + 3*self.sigma)*(self.beta1_deg/90)**(1.09*self.sigma + 1.67)
86 |
87 | @cached_property
88 | def i_star_0_10(self):
89 | "nominal incidence angle theta=0, tbc=0.10 (deg)"
90 | # seal pitch (dimensionless)
91 | p = (1/160)*self.sigma**3 + 0.914
92 | return ((self.beta1_deg**p)/(5 + 46*np.exp(-2.3*self.sigma))) - 0.1*self.sigma**3*np.exp((self.beta1_deg - 70)/4)
93 |
94 | def get_i_star_deg(self, theta_deg: Union[float, np.ndarray]):
95 | "nominal incidence angle (deg)"
96 | return theta_deg*self.n + self.kti*self.i_star_0_10*self.Ksh
97 |
98 | def get_delta_star_deg(self, theta_deg: Union[float, np.ndarray]):
99 | "nominal deviation angle (deg)"
100 | return self.Ksh*self.Ktdelta*self.delta_star_0_10 + theta_deg*self.m
101 |
102 | def get_metal_angle_offset(self, iterations: int):
103 | i_star_deg, delta_star_deg = 0, 0
104 | # TODO: make this more efficient with Numba
105 | for _ in range(iterations):
106 | metal_angles_deg = MetalAngles(self.beta1_deg, self.beta2_deg, i_star_deg, delta_star_deg)
107 | theta_deg = metal_angles_deg.theta
108 | i_star_deg = self.get_i_star_deg(theta_deg)
109 | delta_star_deg = self.get_delta_star_deg(theta_deg)
110 |
111 | i = np.radians(i_star_deg) * np.sign(self.beta1)
112 | delta = np.radians(delta_star_deg) * np.sign(self.beta2)
113 | return MetalAngleOffset(i, delta)
114 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | su2/
2 | cfd/
3 | generated/
4 | simulations/
5 | paraview
6 | docs
7 | bin/CQ-editor*
8 | bin/CQ-editor/
9 | *.curaprofile
10 | ./cad
11 | *.step
12 | *.geo
13 | *.stl
14 | *.out
15 | *.mod
16 | *.pkl
17 |
18 | # Byte-compiled / optimized / DLL files
19 | __pycache__/
20 | *.py[cod]
21 | *$py.class
22 |
23 | # C extensions
24 | *.so
25 |
26 | # Distribution / packaging
27 | .Python
28 | build/
29 | develop-eggs/
30 | dist/
31 | downloads/
32 | eggs/
33 | .eggs/
34 | lib/
35 | lib64/
36 | parts/
37 | sdist/
38 | var/
39 | wheels/
40 | share/python-wheels/
41 | *.egg-info/
42 | .installed.cfg
43 | *.egg
44 | MANIFEST
45 |
46 | # PyInstaller
47 | # Usually these files are written by a python script from a template
48 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
49 | *.manifest
50 | *.spec
51 |
52 | # Installer logs
53 | pip-log.txt
54 | pip-delete-this-directory.txt
55 |
56 | # Unit test / coverage reports
57 | htmlcov/
58 | .tox/
59 | .nox/
60 | .coverage
61 | .coverage.*
62 | .cache
63 | nosetests.xml
64 | coverage.xml
65 | *.cover
66 | *.py,cover
67 | .hypothesis/
68 | .pytest_cache/
69 | cover/
70 |
71 | # Translations
72 | *.mo
73 | *.pot
74 |
75 | # Django stuff:
76 | *.log
77 | local_settings.py
78 | db.sqlite3
79 | db.sqlite3-journal
80 |
81 | # Flask stuff:
82 | instance/
83 | .webassets-cache
84 |
85 | # Scrapy stuff:
86 | .scrapy
87 |
88 | # Sphinx documentation
89 | docs/_build/
90 |
91 | # PyBuilder
92 | .pybuilder/
93 | target/
94 |
95 | # Jupyter Notebook
96 | .ipynb_checkpoints
97 |
98 | # IPython
99 | profile_default/
100 | ipython_config.py
101 |
102 | # pyenv
103 | # For a library or package, you might want to ignore these files since the code is
104 | # intended to run in multiple environments; otherwise, check them in:
105 | # .python-version
106 |
107 | # pipenv
108 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
109 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
110 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
111 | # install all needed dependencies.
112 | #Pipfile.lock
113 |
114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
115 | __pypackages__/
116 |
117 | # Celery stuff
118 | celerybeat-schedule
119 | celerybeat.pid
120 |
121 | # SageMath parsed files
122 | *.sage.py
123 |
124 | # Environments
125 | .env
126 | .venv
127 | env/
128 | venv/
129 | ENV/
130 | env.bak/
131 | venv.bak/
132 |
133 | # Spyder project settings
134 | .spyderproject
135 | .spyproject
136 |
137 | # Rope project settings
138 | .ropeproject
139 |
140 | # mkdocs documentation
141 | /site
142 |
143 | # mypy
144 | .mypy_cache/
145 | .dmypy.json
146 | dmypy.json
147 |
148 | # Pyre type checker
149 | .pyre/
150 |
151 | # pytype static type analyzer
152 | .pytype/
153 |
154 | # Cython debug symbols
155 | cython_debug/
156 |
157 | # General
158 | .DS_Store
159 | .AppleDouble
160 | .LSOverride
161 |
162 | # Icon must end with two \r
163 | Icon
164 |
165 |
166 | # Thumbnails
167 | ._*
168 |
169 | # Files that might appear in the root of a volume
170 | .DocumentRevisions-V100
171 | .fseventsd
172 | .Spotlight-V100
173 | .TemporaryItems
174 | .Trashes
175 | .VolumeIcon.icns
176 | .com.apple.timemachine.donotpresent
177 |
178 | # Directories potentially created on remote AFP share
179 | .AppleDB
180 | .AppleDesktop
181 | Network Trash Folder
182 | Temporary Items
183 | .apdisk
184 |
185 | *~
186 |
187 | # temporary files which can be created if a process still has a handle open of a deleted file
188 | .fuse_hidden*
189 |
190 | # KDE directory preferences
191 | .directory
192 |
193 | # Linux trash folder which might appear on any partition or disk
194 | .Trash-*
195 |
196 | # .nfs files are created when an open file is removed but is still being accessed
197 | .nfs*
198 |
199 | # Windows thumbnail cache files
200 | Thumbs.db
201 | Thumbs.db:encryptable
202 | ehthumbs.db
203 | ehthumbs_vista.db
204 |
205 | # Dump file
206 | *.stackdump
207 |
208 | # Folder config file
209 | [Dd]esktop.ini
210 |
211 | # Recycle Bin used on file shares
212 | $RECYCLE.BIN/
213 |
214 | # Windows Installer files
215 | *.cab
216 | *.msi
217 | *.msix
218 | *.msm
219 | *.msp
220 |
221 | # Windows shortcuts
222 | *.lnk
223 |
--------------------------------------------------------------------------------
/turbodesigner/cad/blade.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from functools import cached_property
3 | import cadquery as cq
4 | import numpy as np
5 | from turbodesigner.blade.row import BladeRowCadExport
6 | from turbodesigner.cad.common import ExtendedWorkplane, FastenerPredicter
7 |
8 |
9 | @dataclass
10 | class BladeCadModelSpecification:
11 | include_attachment: bool = True
12 | "whether to include attachment (bool)"
13 |
14 | screw_length_padding: float = 0.00
15 | "screw length padding (dimensionless)"
16 |
17 | fastener_diameter_to_attachment_bottom_width: float = 0.25
18 | "blade attachment fastener to disk height (dimensionless)"
19 |
20 |
21 | @dataclass
22 | class BladeCadModel:
23 | blade_row: BladeRowCadExport
24 | "blade row"
25 |
26 | spec: BladeCadModelSpecification = field(default_factory=BladeCadModelSpecification)
27 | "blade cad model specification"
28 |
29 | @cached_property
30 | def lock_screw(self):
31 | return FastenerPredicter.predict_screw(
32 | target_diameter=self.heatset.thread_diameter,
33 | target_length=self.spec.screw_length_padding+self.heatset.nut_thickness
34 | )
35 |
36 | @cached_property
37 | def heatset(self):
38 | return FastenerPredicter.predict_heatset(
39 | target_diameter=self.spec.fastener_diameter_to_attachment_bottom_width*self.blade_row.attachment_bottom_width,
40 | max_thickness=self.blade_row.attachment_height*0.75
41 | )
42 |
43 | @cached_property
44 | def blade_assembly(self):
45 | base_assembly = cq.Assembly()
46 | fastener_assembly = cq.Assembly()
47 |
48 | start_airfoil = self.blade_row.airfoils[0]
49 | airfoil_vertical_offset = np.array([
50 | (np.max(start_airfoil[:, 0]) + np.min(start_airfoil[:, 0]))/2,
51 | (np.max(start_airfoil[:, 1]) + np.min(start_airfoil[:, 1]))/2
52 | ])
53 |
54 | # Hub Airfoil
55 | blade_profile = (
56 | cq.Workplane("XY")
57 | .polyline(start_airfoil - airfoil_vertical_offset)
58 | .close()
59 | )
60 |
61 | # Add all airfoil stations
62 | for i in range(0, len(self.blade_row.radii) - 1):
63 | blade_profile = (
64 | blade_profile
65 | .transformed(offset=cq.Vector(0, 0, self.blade_row.radii[i+1]-self.blade_row.radii[i]))
66 | .polyline(self.blade_row.airfoils[i+1] - airfoil_vertical_offset)
67 | .close()
68 | )
69 |
70 | if self.blade_row.is_rotating:
71 | hub_height_offset = self.blade_row.hub_radius*np.cos((2*np.pi / self.blade_row.number_of_blades) / 2)-self.blade_row.hub_radius
72 | else:
73 | hub_height_offset = 0
74 |
75 | blade_height = self.blade_row.tip_radius-self.blade_row.hub_radius
76 | path = (
77 | cq.Workplane("XZ")
78 | .lineTo(hub_height_offset, blade_height)
79 | )
80 |
81 | attachment_workplane = ExtendedWorkplane("YZ")
82 | if not self.blade_row.is_rotating:
83 | attachment_workplane = (
84 | attachment_workplane
85 | .transformed(rotate=(0, 0, 180), offset=(0, blade_height, 0))
86 | )
87 | # Attachment Profile
88 | attachment_profile = (
89 | attachment_workplane
90 | .polyline(self.blade_row.attachment) # type: ignore
91 | .close()
92 | .extrude(self.blade_row.disk_height*0.5, both=True)
93 |
94 | .faces("Z")
95 | .workplane()
96 | .insertHole(self.heatset, depth=self.heatset.nut_thickness*0.9, baseAssembly=fastener_assembly)
97 | )
98 |
99 | blade_profile = (
100 | blade_profile
101 | .sweep(path, multisection=True, makeSolid=True)
102 | )
103 |
104 | if self.spec.include_attachment:
105 | blade_profile = (
106 | blade_profile
107 | .add(attachment_profile)
108 | )
109 |
110 | base_assembly.add(blade_profile, name="Blade")
111 | base_assembly.add(fastener_assembly, name="Fasteners")
112 |
113 | return base_assembly
114 |
--------------------------------------------------------------------------------
/turbodesigner/cad/common.py:
--------------------------------------------------------------------------------
1 | from typing import Callable, List, Optional
2 | import cadquery as cq
3 | import cq_warehouse.extensions as cq_warehouse_extensions
4 | from cq_warehouse.fastener import SocketHeadCapScrew, HeatSetNut
5 | import numpy as np
6 | import re
7 |
8 |
9 | class ExtendedWorkplane(cq.Workplane):
10 | clearanceHole = cq_warehouse_extensions._clearanceHole
11 | tapHole = cq_warehouse_extensions._tapHole
12 | threadedHole = cq_warehouse_extensions._threadedHole
13 | insertHole = cq_warehouse_extensions._insertHole
14 | pressFitHole = cq_warehouse_extensions._pressFitHole
15 |
16 | def ring(self, radius: float, thickness: float, depth: float) -> "ExtendedWorkplane":
17 | return (
18 | self
19 | .circle(radius)
20 | .circle(radius - thickness)
21 | .extrude(depth)
22 | )
23 |
24 | def truncated_cone(self, start_radius: float, end_radius: float, height: float):
25 | plane_world_coords = self.plane.toWorldCoords((0, 0, 0))
26 | path = cq.Workplane("XZ").moveTo(0, plane_world_coords.z).lineTo(0, height + plane_world_coords.z)
27 |
28 | return (
29 | self
30 | .circle(start_radius)
31 | .transformed(offset=cq.Vector(0, 0, height))
32 | .circle(end_radius)
33 | .sweep(path, multisection=True, makeSolid=True)
34 | .clean()
35 | )
36 |
37 | def hollow_truncated_cone(self, inner_start_radius: float, inner_end_radius: float, height: float, start_thickness: float, end_thickness: float):
38 | outer_radius = inner_start_radius + start_thickness
39 | return (
40 | self
41 | .truncated_cone(outer_radius, outer_radius, height)
42 | .cut(
43 | ExtendedWorkplane("XY")
44 | .truncated_cone(inner_start_radius, inner_end_radius, height)
45 | )
46 | )
47 |
48 | def mutatePoints(self, callback: Callable[[cq.Location], cq.Location]):
49 | for (i, loc) in enumerate(self.objects):
50 | if isinstance(loc, cq.Vertex) or isinstance(loc, cq.Vector):
51 | loc = cq.Location(self.plane, loc.toTuple())
52 |
53 | assert isinstance(loc, cq.Location)
54 | mutated_loc = callback(loc)
55 | self.objects[i] = mutated_loc
56 |
57 | return self
58 |
59 |
60 | class FastenerPredicter:
61 | @staticmethod
62 | def get_nominal_size(
63 | target_diameter: float,
64 | nominal_size_range: List[str],
65 | ):
66 | for nominal_size in nominal_size_range:
67 | nominal_diameter = float(nominal_size.split("-")[0].replace("M", ""))
68 | if target_diameter <= nominal_diameter:
69 | return nominal_size
70 |
71 | raise ValueError(f"nominal size for target diameter {target_diameter} could not be found")
72 |
73 | @staticmethod
74 | def get_nominal_length(
75 | target_length: float,
76 | nominal_length_range: List[float],
77 | ):
78 | for nominal_length in nominal_length_range:
79 | if target_length <= nominal_length:
80 | return nominal_length
81 | raise ValueError(f"nominal length for target length {target_length} could not be found")
82 |
83 |
84 | @staticmethod
85 | def predict_heatset(target_diameter: float, max_thickness: Optional[float] = None, type: str = "Hilitchi"):
86 | nominal_size_range = HeatSetNut.sizes(type)
87 | last_acceptable_height_heatset: Optional[HeatSetNut] = None
88 | for nominal_size in nominal_size_range:
89 | heatset = HeatSetNut(
90 | size=nominal_size,
91 | fastener_type=type,
92 | simple=True,
93 | )
94 | if max_thickness and heatset.nut_thickness <= max_thickness:
95 | last_acceptable_height_heatset = heatset
96 | if target_diameter <= heatset.nut_diameter :
97 | if max_thickness and heatset.nut_thickness > max_thickness:
98 | assert last_acceptable_height_heatset is not None, f"no heasets are valid for max height {max_thickness}, closest heatset {heatset.nut_diameter}"
99 | return last_acceptable_height_heatset
100 | return heatset
101 | raise ValueError(f"nominal size for target diameter {target_diameter} could not be found")
102 |
103 |
104 | @staticmethod
105 | def predict_screw(target_diameter: float, target_length: Optional[float] = None, type: str = "iso4762"):
106 | nominal_length_range = SocketHeadCapScrew.nominal_length_range[type]
107 | predicted_length = nominal_length_range[0]
108 | if target_length:
109 | predicted_length = FastenerPredicter.get_nominal_length(target_length, nominal_length_range)
110 |
111 | nominal_size_range = SocketHeadCapScrew.sizes(type)
112 | predicted_size = FastenerPredicter.get_nominal_size(target_diameter, nominal_size_range)
113 |
114 | return SocketHeadCapScrew(predicted_size, predicted_length, type)
115 |
--------------------------------------------------------------------------------
/turbodesigner/stage.py:
--------------------------------------------------------------------------------
1 | from functools import cached_property
2 | from dataclasses import dataclass
3 | from typing import Optional
4 | import numpy as np
5 | from turbodesigner.blade.row import BladeRow, BladeRowCadExport, MetalAngleMethods
6 | from turbodesigner.blade.vortex.free_vortex import FreeVortex
7 | from turbodesigner.flow_station import FlowStation
8 | from turbodesigner.units import MM
9 |
10 |
11 | @dataclass
12 | class StageBladeProperty:
13 | rotor: float
14 | stator: float
15 |
16 |
17 | @dataclass
18 | class StageCadExport:
19 | rotor: BladeRowCadExport
20 | "rotor blade row"
21 |
22 | stator: BladeRowCadExport
23 | "stator blade row"
24 |
25 | stage_height: float
26 | "stage height (mm)"
27 |
28 | stage_number: int
29 | "stage number"
30 |
31 | row_gap: float
32 | "stage gap (mm)"
33 |
34 | stage_gap: float
35 | "stage gap (mm)"
36 |
37 |
38 | @dataclass
39 | class Stage:
40 | "calculates turbomachinery stage"
41 |
42 | stage_number: int
43 | "stage number"
44 |
45 | Delta_Tt: float
46 | "stage stagnation temperature change between inlet and outlet (K)"
47 |
48 | R: float
49 | "stage reaction (dimensionless)"
50 |
51 | previous_flow_station: FlowStation
52 | "previous flow station (FlowStation)"
53 |
54 | eta_poly: float
55 | "polytropic efficiency (dimensionless)"
56 |
57 | N_stream: int
58 | "number of streams per blade (dimensionless)"
59 |
60 | AR: StageBladeProperty
61 | "aspect ratio (dimensionless)"
62 |
63 | sc: StageBladeProperty
64 | "spacing to chord ratio (dimensionless)"
65 |
66 | tbc: StageBladeProperty
67 | "max thickness to chord (dimensionless)"
68 |
69 | rgc: float
70 | "row gap to chord (dimensionless)"
71 |
72 | sgc: float
73 | "stage gap to chord (dimensionless)"
74 |
75 | metal_angle_method: MetalAngleMethods
76 | "metal angle method"
77 |
78 | next_stage: Optional["Stage"] = None
79 | "next turbomachinery stage"
80 |
81 | def __post_init__(self):
82 | assert isinstance(self.previous_flow_station.radius, float)
83 | self.rm = self.previous_flow_station.radius
84 | self.N = self.previous_flow_station.N
85 |
86 | @cached_property
87 | def Delta_ht(self) -> float:
88 | "enthalpy change between inlet and outlet (J/kg)"
89 | return self.Delta_Tt*self.previous_flow_station.Cp
90 |
91 | @cached_property
92 | def U(self):
93 | "mean blade velocity (m/s)"
94 | U = FlowStation.calc_U(self.N, self.rm)
95 | assert isinstance(U, float)
96 | return U
97 |
98 | @cached_property
99 | def phi(self):
100 | "flow coefficient (dimensionless)"
101 | return self.previous_flow_station.Vm/self.U
102 |
103 | @cached_property
104 | def psi(self):
105 | "loading coefficient (dimensionless)"
106 | return self.Delta_ht/self.U**2
107 |
108 | @cached_property
109 | def Tt2(self):
110 | "outlet total temperature (K)"
111 | return self.inlet_flow_station.Tt + self.Delta_Tt
112 |
113 | @cached_property
114 | def inlet_flow_station(self):
115 | "mid flow station between rotor and stator (FlowStation)"
116 | alpha1 = np.arctan((1 - self.R + -(1/2)*self.psi)/self.phi)
117 | return self.previous_flow_station.copyFlow(
118 | alpha=alpha1,
119 | )
120 |
121 | @cached_property
122 | def mid_flow_station(self):
123 | "mid flow station between rotor and stator (FlowStation)"
124 | alpha2 = np.arctan((1 - self.R + (1/2)*self.psi)/self.phi)
125 | Pt2 = self.inlet_flow_station.Pt*self.PR
126 | return self.inlet_flow_station.copyFlow(
127 | Tt=self.Tt2,
128 | Pt=Pt2,
129 | alpha=alpha2,
130 | )
131 |
132 | @cached_property
133 | def PR(self):
134 | "pressure ratio (dimensionless)"
135 | gamma = self.inlet_flow_station.gamma
136 | return self.TR**(self.eta_poly*gamma/(gamma - 1))
137 |
138 | @cached_property
139 | def TR(self):
140 | "stagnation temperature ratio between stage outlet and inlet (dimensionless)"
141 | return self.Tt2/self.inlet_flow_station.Tt
142 |
143 | @cached_property
144 | def tau(self):
145 | "torque transmitted to stage (N*m)"
146 | return self.inlet_flow_station.mdot*self.rm*(self.mid_flow_station.Vtheta - self.inlet_flow_station.Vtheta)
147 |
148 | @cached_property
149 | def vortex(self):
150 | return FreeVortex(
151 | Um=self.U,
152 | Vm=self.previous_flow_station.Vm,
153 | Rm=self.R,
154 | psi_m=self.psi,
155 | rm=self.rm
156 | )
157 |
158 | @cached_property
159 | def rotor(self):
160 | return BladeRow(
161 | stage_number=self.stage_number,
162 | stage_flow_station=self.inlet_flow_station,
163 | vortex=self.vortex,
164 | AR=self.AR.rotor,
165 | sc=self.sc.rotor,
166 | tbc=self.tbc.rotor,
167 | is_rotating=True,
168 | N_stream=self.N_stream,
169 | metal_angle_method=self.metal_angle_method,
170 | next_stage_flow_station=self.stator.flow_station
171 | )
172 |
173 | @cached_property
174 | def stator(self):
175 | return BladeRow(
176 | stage_number=self.stage_number,
177 | stage_flow_station=self.mid_flow_station,
178 | vortex=self.vortex,
179 | AR=self.AR.stator,
180 | sc=self.sc.stator,
181 | tbc=self.tbc.stator,
182 | is_rotating=False,
183 | N_stream=self.N_stream,
184 | metal_angle_method=self.metal_angle_method,
185 | next_stage_flow_station=None if self.next_stage is None else self.next_stage.rotor.flow_station
186 | )
187 |
188 | def to_cad_export(self):
189 | rotor = self.rotor.to_cad_export()
190 | stator = self.stator.to_cad_export()
191 | stage_height = rotor.disk_height+stator.disk_height
192 | return StageCadExport(
193 | stage_number=self.stage_number,
194 | rotor=rotor,
195 | stator=stator,
196 | stage_height=stage_height,
197 | stage_gap=self.sgc*self.rotor.c*MM,
198 | row_gap=self.rgc*self.rotor.c*MM
199 | )
200 |
--------------------------------------------------------------------------------
/turbodesigner/airfoils/DCA.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 | from functools import cached_property
3 | from typing import Optional, Union
4 | import plotly.graph_objects as go
5 | import numpy as np
6 | from turbodesigner.airfoils.common import get_staggered_coords
7 |
8 | @dataclass
9 | class DCAAirfoil:
10 | c: float
11 | "chord length (length)"
12 |
13 | theta: float
14 | "camber angle (rad)"
15 |
16 | r0: float
17 | "double circular arc airfoil nose radius (length)"
18 |
19 | tb: float
20 | "max thickness (length)"
21 |
22 | xi: float = 0
23 | "stagger angle (rad)"
24 |
25 | arc_weight: float = 0.8
26 | "percentage of how much of x coordinates to load for arc"
27 |
28 | def __post_init__(self):
29 | if self.theta == 0:
30 | self.theta = 1E-5
31 | self.theta_mag = np.abs(self.theta)
32 |
33 |
34 | def get_camber_line(self, xc:Optional[Union[float, np.ndarray]] = None, is_staggered: bool = True, is_centered: bool = True, num_points = 20):
35 | """coordinates of camber line (length)
36 |
37 | num_points: int
38 | number of points
39 | """
40 | # horizontal position of camber or chord line (length)
41 | if xc is None:
42 | xc = np.linspace(-self.c/2, self.c/2, num_points, endpoint=True)
43 | y_sign = np.sign(self.theta)
44 | # radius of curvature (length)
45 | Rc = (self.c/2) * (1/(np.sin(self.theta/2)))
46 |
47 | # camber line y-coordinate origin of radius (length)
48 | yc0 = -Rc * np.cos(self.theta/2)
49 |
50 | # vertical position of camber of chord line (length)
51 | yc = yc0 + np.sqrt(Rc**2 - xc**2) * y_sign
52 |
53 | camber_line = np.array([xc,yc]).T
54 |
55 | if is_centered:
56 | camber_line = camber_line - np.array([0,yc0 + np.sqrt(Rc**2) * y_sign])
57 |
58 | if is_staggered:
59 | camber_line = get_staggered_coords(camber_line, self.xi)
60 | return camber_line
61 |
62 | def get_circle(self, is_left: bool, num_points: int):
63 | """coordinates of circle for DCA airfoil (length)
64 |
65 | is_left: bool
66 | whether it is the left circle of DCA airfoil
67 |
68 | num_points: int
69 | number of points
70 | """
71 |
72 | x_sign = -1 if is_left else 1
73 | x_center = x_sign*(self.c/2 - self.r0*np.cos(self.theta_mag/2))
74 | y_center = self.r0 * np.sin(self.theta_mag/2)
75 |
76 | center_to_end_distance = (x_sign*self.c/2) - x_center
77 | angle_offset = np.pi - np.arccos(center_to_end_distance/self.r0) + np.pi/2
78 | angle = np.linspace(0, np.pi, num_points, endpoint=True) + angle_offset
79 |
80 | x = self.r0 * np.cos(angle) + x_center
81 | y = (self.r0 * np.sin(angle) + y_center) * np.sign(self.theta)
82 |
83 | return np.array([x,y]).T
84 |
85 |
86 | def get_arc(self, is_lower = False, num_points=20):
87 | """y-coordinates of chord line (length)
88 |
89 | is_lower: bool = False
90 | whether arc is lower
91 |
92 | """
93 | # negate tb and r0 if this a lower arc
94 | input_sign = -1 if is_lower else 1
95 | r0 = input_sign*self.r0
96 | tb = input_sign*self.tb
97 |
98 | x = np.linspace(-self.c*self.arc_weight/2, self.c*self.arc_weight/2, num=num_points)
99 |
100 | # camberline coordinate at mid coord (length)
101 | ym = (self.c/2)*np.tan(self.theta_mag/4)
102 |
103 | # radius of circular arc (length)
104 | d = ym + (tb / 2) - r0*np.sin(self.theta_mag/2)
105 | R = (d**2 - (r0**2) + ((self.c/2) - r0 * np.cos(self.theta_mag/2))**2)/(2*(d-r0))
106 |
107 | # origin of circular arc (length)
108 | y0 = ym + (tb/2) - R
109 |
110 | y = np.sign(self.theta) * (y0 + np.sqrt(R**2 - x**2))
111 |
112 | if is_lower and np.abs(y[0]) > self.tb*2:
113 | y = -np.sign(self.theta) * np.ones(num_points) * self.r0
114 |
115 | return np.array([x,y]).T
116 |
117 | def get_coords(
118 | self,
119 | num_arc_points: int = 20,
120 | num_circle_points: int = 10
121 | ):
122 | """double circular arc airfoil coordinates
123 |
124 | num_arc_points: int
125 | number of arc points
126 |
127 | num_circle_points: int
128 | number of circle points
129 |
130 | """
131 | # Info: x coordinates are [:,0] and y coordinates are [:,1]
132 |
133 | # Circles
134 | left_circle = self.get_circle(is_left=True, num_points=num_circle_points)
135 | right_circle = self.get_circle(is_left=False, num_points=num_circle_points)
136 |
137 | # Arcs
138 | upper_arc = self.get_arc(is_lower=False, num_points=num_arc_points)
139 | lower_arc = self.get_arc(is_lower=True, num_points=num_arc_points)
140 | left_circle_start = np.array([left_circle[0]])
141 |
142 | upper_cond = np.where(np.logical_and(upper_arc[:,0] > left_circle[0,0], upper_arc[:,0] < right_circle[-1,0]))
143 | lower_cond = np.where(np.logical_and(lower_arc[:,0] > left_circle[-1,0], lower_arc[:,0] < right_circle[0,0]))
144 |
145 | center = self.get_camber_line(0, is_staggered=False, is_centered=False)
146 | airfoil = np.concatenate(
147 | [
148 | left_circle,
149 | lower_arc[lower_cond],
150 | right_circle,
151 | np.flip(upper_arc[upper_cond], axis=0),
152 | left_circle_start
153 | ]
154 | ) - center
155 |
156 | return get_staggered_coords(airfoil, self.xi)
157 |
158 | def visualize(
159 | self,
160 | fig=go.Figure(),
161 | show=True,
162 | num_arc_points: int = 20,
163 | num_circle_points: int = 10
164 | ):
165 | camber_xy = self.get_camber_line(num_points=num_arc_points)
166 | fig.add_trace(go.Scatter(
167 | x=camber_xy[:, 0],
168 | y=camber_xy[:, 1],
169 | ))
170 |
171 | xy = self.get_coords(num_arc_points, num_circle_points)
172 | fig.add_trace(go.Scatter(
173 | x=xy[:, 0],
174 | y=xy[:, 1],
175 | fill="toself"
176 | ))
177 |
178 | fig.layout.yaxis.scaleanchor="x" # type: ignore
179 | if show:
180 | fig.show()
181 |
--------------------------------------------------------------------------------
/turbodesigner/turbomachinery.py:
--------------------------------------------------------------------------------
1 | from functools import cached_property
2 | from dataclasses import dataclass
3 | import json
4 | from typing import Literal, Union
5 | import numpy as np
6 | from turbodesigner.blade.row import MetalAngleMethods
7 | from turbodesigner.flow_station import FlowStation
8 | from turbodesigner.stage import Stage, StageBladeProperty, StageCadExport
9 | from dacite.core import from_dict
10 | Number = Union[int, float]
11 |
12 |
13 | @dataclass
14 | class TurbomachineryCadExport:
15 | stages: list[StageCadExport]
16 | "turbomachinery stages"
17 |
18 |
19 | @dataclass
20 | class Turbomachinery:
21 | gamma: float
22 | "ratio of specific heats (dimensionless)"
23 |
24 | cx: float
25 | "inlet flow velocity in the radial and axial plane (m/s)"
26 |
27 | N: float
28 | "rotational speed (rpm)"
29 |
30 | Rs: float
31 | "specific gas constant (J/(kg*K))"
32 |
33 | mdot: float
34 | "mass flow rate (kg/s)"
35 |
36 | PR: float
37 | "pressure ratio (dimensionless)"
38 |
39 | Pt: float
40 | "ambient total pressure (Pa)"
41 |
42 | Tt: float
43 | "ambient total temperature (K)"
44 |
45 | eta_isen: float
46 | "isentropic efficiency (dimensionless)"
47 |
48 | N_stg: int
49 | "number of stages (dimensionless)"
50 |
51 | B_in: Union[Number, list[Number]]
52 | "inlet blockage factor (dimensionless)"
53 |
54 | B_out: Union[Number, list[Number]]
55 | "outlet blockage factor (dimensionless)"
56 |
57 | ht: float
58 | "hub to tip ratio (dimensionless)"
59 |
60 | N_stream: int
61 | "number of streams per blade (dimensionless)"
62 |
63 | Delta_T0_stg: Union[list[float], Literal["equal"]]
64 | "array of stage stagnation temperature rises between inlet and outlet (K)"
65 |
66 | R_stg: Union[Number, list[Number]]
67 | "array of stage reaction rates (dimensionless)"
68 |
69 | rgc: Union[Number, list[Number]]
70 | "row gap to chord (dimensionless)"
71 |
72 | sgc: Union[Number, list[Number]]
73 | "stage gap to chord (dimensionless)"
74 |
75 | AR: Union[StageBladeProperty, list[StageBladeProperty]]
76 | "aspect ratios (dimensionless)"
77 |
78 | sc: Union[StageBladeProperty, list[StageBladeProperty]]
79 | "spacing to chord ratios (dimensionless)"
80 |
81 | tbc: Union[StageBladeProperty, list[StageBladeProperty]]
82 | "max thickness to chords (dimensionless)"
83 |
84 | metal_angle_method: MetalAngleMethods = "JohnsenBullock"
85 | "metal angle method"
86 |
87 | @cached_property
88 | def Tt2(self):
89 | "outlet stagnation temperature (K)"
90 | return self.Tt*self.PR**((self.gamma - 1)/(self.eta_poly*self.gamma))
91 |
92 | @cached_property
93 | def Pt2(self):
94 | "stagnation outlet pressure (Pa)"
95 | return self.Pt*self.PR
96 |
97 | @cached_property
98 | def eta_poly(self):
99 | "polytropic efficiency (dimensionless)"
100 | return (self.gamma - 1)*np.log(self.PR)/(self.gamma*np.log((self.eta_isen + self.PR**((self.gamma - 1)/self.gamma) - 1)/self.eta_isen))
101 |
102 | @cached_property
103 | def inlet_flow_station(self):
104 | "inlet flow station (FlowStation)"
105 | flow_station = FlowStation(
106 | gamma=self.gamma,
107 | Rs=self.Rs,
108 | Tt=self.Tt,
109 | Pt=self.Pt,
110 | Vm=self.cx,
111 | mdot=self.mdot,
112 | B=self.B_in[0] if isinstance(self.B_in, list) else self.B_in,
113 | N=self.N,
114 | )
115 | flow_station.set_radius(self.ht)
116 | return flow_station
117 |
118 | @cached_property
119 | def outlet_flow_station(self):
120 | "outlet flow station (FlowStation)"
121 | return FlowStation(
122 | gamma=self.gamma,
123 | Rs=self.Rs,
124 | Tt=self.Tt2,
125 | Pt=self.Pt2,
126 | Vm=self.cx,
127 | mdot=self.mdot,
128 | B=self.B_out[-1] if isinstance(self.B_out, list) else self.B_out,
129 | N=self.N,
130 | radius=self.inlet_flow_station.radius
131 | )
132 |
133 | @cached_property
134 | def Delta_T0(self):
135 | "stagnation temperature change between outlet and inlet (dimensionless)"
136 | return self.outlet_flow_station.Tt - self.inlet_flow_station.Tt
137 |
138 | @cached_property
139 | def TR(self):
140 | "stagnation temperature ratio between outlet and inlet (dimensionless)"
141 | return self.outlet_flow_station.Tt/self.inlet_flow_station.Tt
142 |
143 | @cached_property
144 | def stages(self):
145 | "turbomachinery stages (list[Stage])"
146 | if isinstance(self.Delta_T0_stg, list):
147 | assert len(self.Delta_T0_stg) == self.N_stg, "Delta_T0 quantity does not equal N_stg"
148 | else:
149 | assert self.Delta_T0_stg == "equal", f"'{self.Delta_T0_stg}' for Delta_T0_stg is invalid"
150 |
151 | if isinstance(self.R_stg, list):
152 | assert len(self.R_stg) == self.N_stg, "R quantity does not equal N_stg"
153 | assert self.R_stg[self.N_stg-1] == 0.5, "Last stage reaction only supports R=0.5"
154 | else:
155 | assert self.R_stg == 0.5, "Last stage reaction only supports R=0.5"
156 |
157 | if isinstance(self.AR, list):
158 | assert len(self.AR) == self.N_stg, "AR quantity does not equal N_stg"
159 | if isinstance(self.sc, list):
160 | assert len(self.sc) == self.N_stg, "sc quantity does not equal N_stg"
161 | if isinstance(self.tbc, list):
162 | assert len(self.tbc) == self.N_stg, "tbc quantity does not equal N_stg"
163 |
164 | previous_flow_station = self.inlet_flow_station
165 | stages: list[Stage] = []
166 | # TODO: make this more efficient with Numba
167 | for i in range(self.N_stg):
168 | stage = Stage(
169 | stage_number=i+1,
170 | Delta_Tt=self.Delta_T0_stg[i] if isinstance(self.Delta_T0_stg, list) else self.Delta_T0/self.N_stg,
171 | R=self.R_stg[i] if isinstance(self.R_stg, list) else self.R_stg,
172 | previous_flow_station=previous_flow_station,
173 | eta_poly=self.eta_poly,
174 | N_stream=self.N_stream,
175 | AR=self.AR[i] if isinstance(self.AR, list) else self.AR,
176 | sc=self.sc[i] if isinstance(self.sc, list) else self.sc,
177 | tbc=self.tbc[i] if isinstance(self.tbc, list) else self.tbc,
178 | rgc=self.rgc[i] if isinstance(self.rgc, list) else self.rgc,
179 | sgc=self.sgc[i] if isinstance(self.sgc, list) else self.sgc,
180 | metal_angle_method=self.metal_angle_method
181 | )
182 | previous_flow_station = stage.mid_flow_station
183 | if i > 0 and i < self.N_stg:
184 | stages[i-1].next_stage = stage
185 | stages.append(stage)
186 |
187 | return stages
188 |
189 | def to_cad_export(self):
190 | return TurbomachineryCadExport(
191 | stages=[
192 | stage.to_cad_export() for stage in self.stages
193 | ]
194 | )
195 |
196 | @staticmethod
197 | def from_dict(obj) -> "Turbomachinery":
198 | return from_dict(data_class=Turbomachinery, data=obj)
199 |
200 | @staticmethod
201 | def from_file(file_name: str) -> "Turbomachinery":
202 | with open(file_name, "r") as fp:
203 | obj = json.load(fp)
204 | return from_dict(data_class=Turbomachinery, data=obj)
205 |
--------------------------------------------------------------------------------
/turbodesigner/attachments/firetree.py:
--------------------------------------------------------------------------------
1 | from functools import cached_property
2 | from typing import Optional
3 | import numpy as np
4 | from dataclasses import dataclass
5 | import plotly.graph_objects as go
6 |
7 | def get_line(
8 | y: np.ndarray,
9 | point1: np.ndarray,
10 | point2: np.ndarray,
11 | y_int: Optional[int] = None
12 | ):
13 | x2, y2 = point2[0], point2[1]
14 | x1, y1 = point1[0], point1[1]
15 | m = (y2 - y1) / (x2 - x1)
16 | b = y2 - m*x2 if y_int is None else 0
17 | x = (y-b)/m
18 | return np.array([x, y]).T
19 |
20 |
21 | def get_arc(
22 | lower_point: np.ndarray,
23 | upper_point: np.ndarray,
24 | radius: float,
25 | center: np.ndarray,
26 | num_points: int,
27 | is_clockwise: bool = True,
28 | endpoint: bool = True
29 | ):
30 | lower_distance = lower_point - center
31 | upper_distance = upper_point - center
32 | angle1 = np.arctan2(lower_distance[1], lower_distance[0])
33 | angle2 = np.arctan2(upper_distance[1], upper_distance[0])
34 |
35 | if not is_clockwise:
36 | angle1 = angle1 + 2*np.pi
37 |
38 | angle = np.linspace(angle1, angle2, num_points, endpoint=endpoint)
39 | return center + np.array([
40 | radius * np.cos(angle),
41 | radius * np.sin(angle),
42 | ]).T
43 |
44 |
45 | @dataclass
46 | class FirtreeAttachment:
47 | gamma: float
48 | "angle of upper flank line (rad)"
49 |
50 | beta: float
51 | "angle of lower flank line (rad)"
52 |
53 | ll: float
54 | "lower flank line length (m)"
55 |
56 | lu: float
57 | "upper flank line length (m)"
58 |
59 | Ri: float
60 | "inner circle radius"
61 |
62 | Ro: float
63 | "outer circle radius"
64 |
65 | R_dove: float
66 | "dove circle radius"
67 |
68 | max_length: float
69 | "max length of attachment"
70 |
71 | num_stages: int
72 | "number of stages"
73 |
74 | disk_radius: float
75 | "disk radius of attachment"
76 |
77 | tolerance: float
78 | "attachment side tolerance"
79 |
80 | include_top_arc: bool = True
81 | "whether or not to include top arc"
82 |
83 | num_arc_points: int = 20
84 | "number of arc points"
85 |
86 | def __post_init__(self):
87 | self.origin = np.array([0, 0])
88 |
89 | # Outer Circle
90 | self.outer_circle_lower_tangent = self.origin + (
91 | np.array([self.ll*np.cos(self.beta), self.ll*np.sin(self.beta)])
92 | )
93 | self.outer_circle_upper_tangent = self.outer_circle_lower_tangent + (
94 | np.array([0, 2*self.Ro*np.cos(self.gamma)])
95 | )
96 | self.outer_circle_tanget_intersect = self.outer_circle_lower_tangent + (
97 | np.array([self.Ro*(1 - np.sin(self.gamma)**2)/np.sin(self.gamma), self.Ro*np.cos(self.gamma),])
98 | )
99 | self.outer_circle_center = self.outer_circle_lower_tangent + (
100 | np.array([-self.Ro*np.sin(self.gamma), self.Ro*np.cos(self.gamma)])
101 | )
102 |
103 | # Inner Circle
104 | self.inner_circle_lower_tangent = self.outer_circle_upper_tangent + (
105 | np.array([-self.lu*np.cos(self.gamma), self.lu*np.sin(self.gamma)])
106 | )
107 | self.inner_circle_upper_tangent = self.inner_circle_lower_tangent + (
108 | np.array([0, 2*self.Ri*np.cos(self.gamma)])
109 | )
110 | self.inner_circle_center = self.inner_circle_lower_tangent + (
111 | np.array([self.Ri*np.sin(self.beta), self.Ri*np.cos(self.beta)])
112 | )
113 |
114 | # Dove
115 | self.dove_circle_center = np.array([self.R_dove*np.sin(self.beta), -self.R_dove*np.cos(self.beta)])
116 | self.dove_lower_point = self.dove_circle_center + np.array([0,-self.R_dove])
117 |
118 | @cached_property
119 | def dove_arc(self):
120 | "calculates firtree dove arc"
121 | return get_arc(self.dove_lower_point, self.origin, self.R_dove, self.dove_circle_center, self.num_arc_points, is_clockwise=False)
122 |
123 | def get_top_shape(self, include_tolerance: bool):
124 | "calculates firtree top arc or line segement"
125 | max_length = self.max_length + self.tolerance if include_tolerance else self.max_length
126 | top_arc_left_point = self.left_side[-1]
127 | top_arc_right_point = top_arc_left_point + np.array([max_length, 0])
128 |
129 | if self.include_top_arc:
130 | sector_angle = 2*np.arcsin((max_length/2)/self.disk_radius)
131 | top_arc_height = self.disk_radius - (max_length/2)/np.tan(sector_angle/2)
132 | disk_center = np.array([0,top_arc_left_point[1]-self.disk_radius+top_arc_height])
133 | return get_arc(top_arc_left_point, top_arc_right_point, self.disk_radius, disk_center, self.num_arc_points)
134 |
135 | return np.concatenate([[top_arc_left_point], [top_arc_right_point]])
136 |
137 | def get_stage(self, end_stage: bool = False):
138 | "calculates firtree single stage coordinates"
139 | # Flank Lines
140 | yl = np.linspace(self.origin[1], self.outer_circle_lower_tangent[1], 2, endpoint=False)
141 | lower_flank_line = get_line(yl, self.origin, self.outer_circle_lower_tangent)
142 |
143 | yu = np.linspace(self.outer_circle_upper_tangent[1], self.inner_circle_lower_tangent[1], 2, endpoint=False)
144 | upper_flank_line = get_line(yu, self.outer_circle_tanget_intersect, self.outer_circle_upper_tangent)
145 |
146 | # Arcs
147 | outer_arc = get_arc(self.outer_circle_lower_tangent, self.outer_circle_upper_tangent, self.Ro, self.outer_circle_center, self.num_arc_points, endpoint=False)
148 | inner_arc = get_arc(self.inner_circle_lower_tangent, self.inner_circle_upper_tangent, self.Ri, self.inner_circle_center, self.num_arc_points, is_clockwise=False)
149 |
150 | stage_elements = [lower_flank_line,outer_arc, upper_flank_line]
151 | if not end_stage:
152 | stage_elements.append(inner_arc)
153 |
154 | return np.concatenate(stage_elements)
155 |
156 | @cached_property
157 | def left_side(self) -> np.ndarray:
158 | "calculates firtree attachment left side coordinates"
159 | stage = self.get_stage()
160 | attachment_stage_side:Optional[np.ndarray] = None
161 | assert self.num_stages > 0, "num stages must greater than 0"
162 | # TODO: make this more efficient with Numba
163 | for i in range(self.num_stages):
164 | next_stage = stage
165 | if attachment_stage_side is not None:
166 | attachment_stage_side = np.concatenate([
167 | attachment_stage_side[:-1],
168 | next_stage + attachment_stage_side[-1]
169 | ])
170 | else:
171 | attachment_stage_side = next_stage
172 |
173 | # offset side to max length
174 | assert attachment_stage_side is not None
175 | attachment_center_offset = np.array([-attachment_stage_side[-1][0]-self.max_length/2, 0])
176 | return np.concatenate([self.dove_arc[:-1], attachment_stage_side]) + attachment_center_offset
177 |
178 |
179 | def get_coords(self, include_tolerance: bool):
180 | "calculates firtree attachment coordinates"
181 | top_arc = self.get_top_shape(include_tolerance)
182 | left_side = self.left_side + np.array([-self.tolerance/2, 0]) if include_tolerance else self.left_side
183 | attachment_right_side = np.flip(left_side, axis=0) * np.array([-1, 1])
184 | attachment = np.concatenate([left_side[:-1], top_arc[:-1], attachment_right_side, [left_side[0]]])
185 | return attachment + np.array([0, -np.max(attachment[:,1])])
186 |
187 | @cached_property
188 | def coords(self):
189 | return self.get_coords(include_tolerance=False)
190 |
191 | @cached_property
192 | def coords_with_tolerance(self):
193 | return self.get_coords(include_tolerance=True)
194 |
195 | @cached_property
196 | def height(self):
197 | return np.max(self.coords[:, 1]) - np.min(self.coords[:, 1]) # type: ignore
198 |
199 | @cached_property
200 | def bottom_width(self):
201 | return np.abs(self.left_side[0][0])*2
202 |
203 | def visualize(self):
204 | fig = go.Figure()
205 | fig.add_trace(go.Scatter(
206 | x=self.coords[:, 0],
207 | y=self.coords[:, 1],
208 | ))
209 | fig.layout.yaxis.scaleanchor = "x"
210 | fig.show()
211 |
212 |
--------------------------------------------------------------------------------
/turbodesigner/flow_station.py:
--------------------------------------------------------------------------------
1 | from functools import cached_property
2 | from dataclasses import dataclass
3 | from typing import Optional, Union
4 | import numpy as np
5 |
6 | PROP_NON_STREAM_ERROR = "Property not allowed with streams"
7 |
8 |
9 | class FluidConstants:
10 | MU_REF = 1.73E-5
11 | "reference dynamic viscocity at sea level ((N*s)/m**2)"
12 |
13 | PT_REF = 101325
14 | "reference pressure at sea level (Pa)"
15 |
16 | T0_REF = 288.15
17 | "reference temperature at sea level (K)"
18 |
19 | C = 110.4
20 | "sutherland constant (K)"
21 |
22 |
23 | @dataclass
24 | class FlowStation:
25 | "calculates flow station for an ideal gas"
26 |
27 | gamma: float = np.nan
28 | "ratio of specific heats (dimensionless)"
29 |
30 | Rs: float = np.nan
31 | "specific gas constant (J/(kg*K))"
32 |
33 | Tt: float = np.nan
34 | "total temperature (K)"
35 |
36 | Pt: float = np.nan
37 | "total pressure (Pa)"
38 |
39 | Vm: float = np.nan
40 | "meridional flow velocity (m/s)"
41 |
42 | mdot: float = np.nan
43 | "mass flow rate (kg/s)"
44 |
45 | B: float = 0.0
46 | "blockage factor (dimensionless)"
47 |
48 | alpha: Union[float, np.ndarray] = np.nan
49 | "absolute flow angle (rad)"
50 |
51 | N: float = np.nan
52 | "rotational speed (rpm)"
53 |
54 | radius: Union[float, np.ndarray] = np.nan
55 | "flow radius (m)"
56 |
57 | mixture: str = "Air"
58 | "mixture"
59 |
60 | is_stream: bool = False
61 | "whether station is 1D stream"
62 |
63 | def copyFlow(
64 | self,
65 | Tt: Optional[float] = None,
66 | Pt: Optional[float] = None,
67 | Vm: Optional[float] = None,
68 | mdot: Optional[float] = None,
69 | alpha: Optional[Union[float, np.ndarray]] = None,
70 | radius: Optional[Union[float, np.ndarray]] = None,
71 | ):
72 | "copies all elements of FlowStation (FlowStation)"
73 | return FlowStation(
74 | gamma=self.gamma,
75 | Rs=self.Rs,
76 | Tt=self.Tt if Tt is None else Tt,
77 | Pt=self.Pt if Pt is None else Pt,
78 | Vm=self.Vm if Vm is None else Vm,
79 | mdot=self.mdot if mdot is None else mdot,
80 | B=self.B,
81 | alpha=self.alpha if alpha is None else alpha,
82 | N=self.N,
83 | radius=self.radius if radius is None else radius,
84 | mixture=self.mixture
85 | )
86 |
87 | def copyStream(
88 | self,
89 | alpha: Optional[Union[float, np.ndarray]] = None,
90 | radius: Optional[Union[float, np.ndarray]] = None,
91 | ):
92 | """copies stream elements of FlowStation (FlowStation)
93 |
94 | excludes:
95 | * mdot - mass flow rate
96 |
97 | """
98 | return FlowStation(
99 | gamma=self.gamma,
100 | Rs=self.Rs,
101 | Tt=self.Tt,
102 | Pt=self.Pt,
103 | Vm=self.Vm,
104 | alpha=self.alpha if alpha is None else alpha,
105 | N=self.N,
106 | radius=self.radius if radius is None else radius,
107 | is_stream=True,
108 | mixture=self.mixture
109 | )
110 |
111 | @cached_property
112 | def h(self):
113 | "static enthalpy (J/kg*K)"
114 | return self.T*self.Cp
115 |
116 | @cached_property
117 | def ht(self):
118 | "total enthalpy (J/kg*K)"
119 | return self.h + (self.V**2)/2
120 |
121 | @cached_property
122 | def Cp(self):
123 | "specific heat at constant pressure (J/(kg*K))"
124 | return self.Rs*self.gamma/(self.gamma - 1)
125 |
126 | @cached_property
127 | def T(self):
128 | "static temperature (K)"
129 | return self.Tt - (self.V**2)/(2*self.Cp)
130 |
131 | @cached_property
132 | def Ttr(self):
133 | "total realtive temperature (K)"
134 | return self.Tt + (self.W**2 - self.V**2)/(2*self.Cp)
135 |
136 | @cached_property
137 | def P(self):
138 | "static pressure (Pa)"
139 | return self.Pt*(self.T/self.Tt)**(self.gamma/(self.gamma - 1))
140 |
141 | @cached_property
142 | def Ptr(self):
143 | "total relative pressure (Pa)"
144 | return self.Pt*(self.Ttr/self.Tt)**(self.gamma/(self.gamma - 1))
145 |
146 | @cached_property
147 | def rho(self):
148 | "density (kg/m**3)"
149 | return self.P/(self.T*self.Rs)
150 |
151 | @cached_property
152 | def q(self):
153 | "dynamic pressure (Pa)"
154 | return 0.5*self.rho*self.Vm**2
155 |
156 | @cached_property
157 | def a(self):
158 | "speed of sound in medium (m/s)"
159 | return np.sqrt(self.T*self.Rs*self.gamma)
160 |
161 | @cached_property
162 | def mu(self):
163 | "dynamic velocity using Sutherland's formula ((N*s)/m**2)"
164 | return FluidConstants.MU_REF * ((self.T / FluidConstants.T0_REF)**1.5) * ((FluidConstants.T0_REF + FluidConstants.C) / (self.T + FluidConstants.C))
165 |
166 | @cached_property
167 | def MN(self):
168 | "mach number (dimensionless)"
169 | return self.Vm/self.a
170 |
171 | @cached_property
172 | def Vcr(self):
173 | "critical velocity (m/s)"
174 | return np.sqrt(((2*self.gamma)/(self.gamma+1)) * self.Rs*self.Tt)
175 |
176 | @cached_property
177 | def U(self):
178 | "blade velocity (m/s)"
179 | return FlowStation.calc_U(self.N, self.radius)
180 |
181 | @cached_property
182 | def omega(self):
183 | "blade angular velocity (rad/s)"
184 | return self.U/self.radius
185 |
186 | @cached_property
187 | def Vtheta(self):
188 | "absolute tangential velocity (m/s)"
189 | return self.Vm*np.tan(self.alpha)
190 |
191 | @cached_property
192 | def V(self):
193 | "absolute flow velocity (m/s)"
194 | if np.isnan(self.alpha).all():
195 | return self.Vm
196 | return self.Vm/np.cos(self.alpha)
197 |
198 | @cached_property
199 | def Wtheta(self):
200 | "relative tangential flow velocity (m/s)"
201 | return self.Vtheta - self.U
202 |
203 | @cached_property
204 | def beta(self):
205 | "relative flow angle (rad)"
206 | return np.arctan(self.Wtheta/self.Vm)
207 |
208 | @cached_property
209 | def W(self):
210 | "relative flow velocity (m/s)"
211 | return self.Vm/np.cos(self.beta)
212 |
213 | # %% Annular Properties
214 | @cached_property
215 | def A_flow(self):
216 | "cross-sectional flow area (m**2)"
217 | assert not self.is_stream, PROP_NON_STREAM_ERROR
218 | return self.mdot/(self.rho*self.Vm)
219 |
220 | @cached_property
221 | def A_phys(self):
222 | "physical cross sectional area (m**2)"
223 | return self.A_flow*(self.B + 1)
224 |
225 | @cached_property
226 | def outer_radius(self):
227 | "flow outer radius (m)"
228 | assert not self.is_stream, PROP_NON_STREAM_ERROR
229 | return self.A_phys/(4*np.pi*self.radius) + self.radius
230 |
231 | @cached_property
232 | def inner_radius(self):
233 | "flow inner radius (m)"
234 | assert not self.is_stream, PROP_NON_STREAM_ERROR
235 | return 2*self.radius - self.outer_radius
236 |
237 | @staticmethod
238 | def calc_radius_from_ht(ht: float, A_phys: Union[float, np.ndarray]):
239 | """calculates radius from hub to tip ratio
240 |
241 | Parameters
242 | ==========
243 |
244 | ht: float
245 | hub to tip ratio (dimensionless)
246 |
247 | A_phys: float
248 | physical cross sectional area (m**2)
249 |
250 | """
251 |
252 | outer_radius = np.sqrt(A_phys / (np.pi*(1-ht**2)))
253 | inner_radius = ht * outer_radius
254 | return (outer_radius + inner_radius) / 2
255 |
256 | @staticmethod
257 | def calc_U(N: float, radius: Union[float, np.ndarray]):
258 | """calculates blade velocity
259 |
260 | Parameters
261 | ==========
262 |
263 | N: float
264 | rotational speed (rpm)
265 |
266 | radius: float
267 | blade radius (m)
268 |
269 | """
270 |
271 | return 2*np.pi*N*radius/60
272 |
273 | def set_radius(self, ht: float):
274 | """sets radius from hub to tip ratio
275 |
276 | Parameters
277 | ==========
278 |
279 | ht: float
280 | hub to tip ratio (dimensionless)
281 | """
282 | self.radius = FlowStation.calc_radius_from_ht(ht, self.A_phys)
283 |
--------------------------------------------------------------------------------
/turbodesigner/exporter.py:
--------------------------------------------------------------------------------
1 | from typing import Callable, Optional
2 | import numpy as np
3 | import pandas as pd
4 | from turbodesigner.stage import Stage
5 | from turbodesigner.turbomachinery import Turbomachinery
6 | from turbodesigner.units import DEG, BAR
7 |
8 |
9 | def get_hub_tip_dict_from_export(
10 | stage: Stage,
11 | table_dict: dict[str, dict],
12 | export_dict: dict,
13 | group_name: Optional[str] = None
14 | ):
15 | for (key, value) in export_dict.items():
16 | group_name = group_name or f"Stage {stage.stage_number}"
17 | table_dict["Hub"][(group_name, key)] = value[0] if isinstance(value, np.ndarray) else value
18 | table_dict["Mean"][(group_name, key)] = np.median(value) if isinstance(value, np.ndarray) else value
19 | table_dict["Tip"][(group_name, key)] = value[-1] if isinstance(value, np.ndarray) else value
20 | return table_dict
21 |
22 |
23 | def get_hub_mean_tip_table(
24 | turbomachinery: Turbomachinery,
25 | to_export_dict: Callable[[Stage], dict],
26 | is_multi_row=False
27 | ):
28 | table = {
29 | "Hub": dict(),
30 | "Mean": dict(),
31 | "Tip": dict()
32 | }
33 |
34 | for stage in turbomachinery.stages:
35 | export_dict = to_export_dict(stage)
36 | if is_multi_row:
37 | table = get_hub_tip_dict_from_export(stage, table, export_dict["Rotor"], f"Stage {stage.stage_number} - Rotor")
38 | table = get_hub_tip_dict_from_export(stage, table, export_dict["Stator"], f"Stage {stage.stage_number} - Stator")
39 | else:
40 | table = get_hub_tip_dict_from_export(stage, table, export_dict)
41 |
42 | return pd.DataFrame(table)
43 |
44 |
45 | def get_rotor_stator_table(turbomachinery: Turbomachinery, to_export_dict: Callable[[Stage], dict]):
46 | table_dict = dict()
47 | for stage in turbomachinery.stages:
48 | export_dict = to_export_dict(stage)
49 | for (key, value) in export_dict.items():
50 | if key not in table_dict:
51 | table_dict[key] = dict()
52 | table_dict[key][(f"Stage {stage.stage_number}", "Rotor")] = value["Rotor"]
53 | table_dict[key][(f"Stage {stage.stage_number}", "Stator")] = value["Stator"]
54 | return pd.DataFrame(table_dict)
55 |
56 |
57 | class TurbomachineryExporter:
58 | @staticmethod
59 | def turbomachinery_properties(turbomachinery: Turbomachinery):
60 | properties = {
61 | "gamma (dimensionless)": turbomachinery.gamma,
62 | "cx (m/s)": turbomachinery.cx,
63 | "N (rpm)": turbomachinery.N,
64 | "Rs (J/(kgK))": turbomachinery.Rs,
65 | "mdot (kg/s)": turbomachinery.mdot,
66 | "PR (dimensionless)": turbomachinery.PR,
67 | "Pt1 (bar)": turbomachinery.Pt*BAR,
68 | "Tt1 (K)": turbomachinery.Tt,
69 | "eta_isen (dimensionless)": turbomachinery.eta_isen,
70 | "eta_poly (dimensionless)": turbomachinery.eta_poly,
71 | "N_stg": turbomachinery.N_stg,
72 | "B_in (dimensionless)": turbomachinery.B_in,
73 | "B_out (dimensionless)": turbomachinery.B_out,
74 | "ht (dimensionless)": turbomachinery.ht,
75 | }
76 | return pd.DataFrame.from_dict(properties, orient='index')
77 |
78 | @staticmethod
79 | def stage_properties(turbomachinery: Turbomachinery):
80 | return pd.DataFrame(
81 | [
82 | {
83 | "Stage": stage.stage_number,
84 | "Delta_Tt (K)": stage.Delta_Tt,
85 | "Delta_ht (J/kg)": stage.Delta_ht,
86 | "PR (dimensionless)": stage.PR,
87 | "R (dimensionless)": stage.R,
88 | "phi (dimensionless)": stage.phi,
89 | "psi (dimensionless)": stage.psi
90 | }
91 | for stage in turbomachinery.stages
92 | ],
93 | )
94 |
95 | @staticmethod
96 | def stage_fluid_properties(turbomachinery: Turbomachinery):
97 | return pd.DataFrame(
98 | [
99 | {
100 | "Stage": stage.stage_number,
101 | "Tt1 (K)": stage.inlet_flow_station.Tt,
102 | "Pt1 (bar)": stage.inlet_flow_station.Pt * BAR,
103 | "ht1 (J/kg*K)": stage.inlet_flow_station.ht,
104 | "T1 (K)": stage.inlet_flow_station.T,
105 | "P1 (bar)": stage.inlet_flow_station.P * BAR,
106 | "H1 (K)": stage.inlet_flow_station.h,
107 | "rho1 (kg/m^3)": stage.inlet_flow_station.rho,
108 |
109 | "Tt2 (K)": stage.mid_flow_station.Tt,
110 | "Pt2 (bar)": stage.mid_flow_station.Pt * BAR,
111 | "ht2 (J/kg*K)": stage.mid_flow_station.ht,
112 | "T2 (K)": stage.mid_flow_station.T,
113 | "P2 (bar)": stage.mid_flow_station.P * BAR,
114 | "H2 (K)": stage.mid_flow_station.h,
115 | "rho2 (kg/m^3)": stage.mid_flow_station.rho,
116 | }
117 | for stage in turbomachinery.stages
118 | ]
119 | )
120 |
121 | @staticmethod
122 | def annulus(turbomachinery: Turbomachinery):
123 | return get_rotor_stator_table(
124 | turbomachinery,
125 | lambda stage: {
126 | "rh (m)": {
127 | "Rotor": stage.rotor.rh,
128 | "Stator": stage.stator.rh,
129 | },
130 | "rt (m)": {
131 | "Rotor": stage.rotor.rt,
132 | "Stator": stage.stator.rt,
133 | },
134 | "rm (m)": {
135 | "Rotor": stage.rotor.rm,
136 | "Stator": stage.stator.rm,
137 | }
138 | }
139 | )
140 |
141 | @staticmethod
142 | def velocity_triangle(turbomachinery: Turbomachinery):
143 | return get_hub_mean_tip_table(
144 | turbomachinery,
145 | lambda stage: {
146 | "Vm (m/s)": stage.rotor.flow_station.Vm,
147 | "U (m/s)": stage.rotor.flow_station.U,
148 | "Vθ1 (m/s)": stage.rotor.flow_station.Vtheta,
149 | "V1 (m/s)": stage.rotor.flow_station.V,
150 | "Wθ1 (m/s)": stage.rotor.flow_station.Wtheta,
151 | "W1 (m/s)": stage.rotor.flow_station.W,
152 | "beta1 (deg)": stage.rotor.flow_station.beta * DEG,
153 | "alpha1 (deg)": stage.rotor.flow_station.alpha * DEG,
154 | "Vθ2 (m/s)": stage.stator.flow_station.Vtheta,
155 | "V2 (m/s)": stage.stator.flow_station.V,
156 | "Wθ2 (m/s)": stage.stator.flow_station.Wtheta,
157 | "W2 (m/s)": stage.stator.flow_station.W,
158 | "beta2 (deg)": stage.stator.flow_station.beta * DEG,
159 | "alpha2 (deg)": stage.stator.flow_station.alpha * DEG,
160 | }
161 | )
162 |
163 | @staticmethod
164 | def blade_angles(turbomachinery: Turbomachinery):
165 | return get_hub_mean_tip_table(
166 | turbomachinery,
167 | lambda stage: {
168 | "Rotor": {
169 | "delta (deg)": stage.rotor.metal_angles.delta * DEG,
170 | "i (deg)": stage.rotor.metal_angles.i * DEG,
171 | "kappa1 (deg)": stage.rotor.metal_angles.kappa1 * DEG,
172 | "kappa2 (deg)": stage.rotor.metal_angles.kappa2 * DEG,
173 | "theta (deg)": stage.rotor.metal_angles.theta * DEG,
174 | "xi (deg)": stage.rotor.metal_angles.xi * DEG,
175 | },
176 | "Stator": {
177 | "delta (deg)": stage.stator.metal_angles.delta * DEG,
178 | "i (deg)": stage.stator.metal_angles.i * DEG,
179 | "kappa1 (deg)": stage.stator.metal_angles.kappa1 * DEG,
180 | "kappa2 (deg)": stage.stator.metal_angles.kappa2 * DEG,
181 | "theta (deg)": stage.stator.metal_angles.theta * DEG,
182 | "xi (deg)": stage.stator.metal_angles.xi * DEG,
183 | }
184 | },
185 | is_multi_row=True
186 | )
187 |
188 | @staticmethod
189 | def blade_properties(turbomachinery: Turbomachinery):
190 | return get_rotor_stator_table(
191 | turbomachinery,
192 | lambda stage: {
193 | "sc (dimensionless)": {
194 | "Rotor": stage.rotor.sc,
195 | "Stator": stage.stator.sc,
196 | },
197 | "AR (dimensionless)": {
198 | "Rotor": stage.rotor.AR,
199 | "Stator": stage.stator.AR,
200 | },
201 | "tbc (dimensionless)": {
202 | "Rotor": stage.rotor.tbc,
203 | "Stator": stage.stator.tbc,
204 | },
205 | "sigma (dimensionless)": {
206 | "Rotor": stage.rotor.sigma,
207 | "Stator": stage.stator.sigma,
208 | },
209 | "c (m)": {
210 | "Rotor": stage.rotor.c,
211 | "Stator": stage.stator.c,
212 | },
213 | "h (m)": {
214 | "Rotor": stage.rotor.h,
215 | "Stator": stage.stator.h,
216 | }
217 | }
218 | )
219 |
--------------------------------------------------------------------------------
/turbodesigner/blade/row.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 | from functools import cached_property
3 | from dataclasses import dataclass
4 | from typing import Literal, Optional
5 | from turbodesigner.airfoils import AirfoilType, DCAAirfoil
6 | from turbodesigner.blade.metal_angle_methods.johnsen_bullock import JohnsenBullockMetalAngleMethod
7 | from turbodesigner.blade.metal_angles import MetalAngles
8 | from turbodesigner.blade.vortex.common import Vortex
9 | from turbodesigner.flow_station import FlowStation
10 | from turbodesigner.attachments.firetree import FirtreeAttachment
11 | import numpy as np
12 | import numpy.typing as npt
13 | from turbodesigner.units import MM
14 | MetalAngleMethods = Literal["EqualsFlowAngles", "JohnsenBullock"]
15 |
16 |
17 | @dataclass
18 | class BladeRowCadExport:
19 | stage_number: int
20 | "stage number"
21 |
22 | disk_height: float
23 | "disk height (mm)"
24 |
25 | hub_radius: float
26 | "blade hub radius (mm)"
27 |
28 | tip_radius: float
29 | "blade hub radius (mm)"
30 |
31 | radii: npt.NDArray[np.float64]
32 | "blade station radius (mm)"
33 |
34 | airfoils: np.ndarray
35 | "airfoil coordinates for each blade radius (mm)"
36 |
37 | attachment: np.ndarray
38 | "attachment coordinates (mm)"
39 |
40 | attachment_with_tolerance: np.ndarray
41 | "attachment coordinates (mm)"
42 |
43 | attachment_height: float
44 | "attachment height (mm)"
45 |
46 | attachment_bottom_width: float
47 | "attachment bottom width (mm)"
48 |
49 | number_of_blades: int
50 | "number of blades"
51 |
52 | twist_angle: int
53 | "twist angle of blade"
54 |
55 | is_rotating: bool
56 | "whether blade is rotating or not"
57 |
58 |
59 | @dataclass
60 | class BladeRow:
61 | "calculates turbomachinery blade row"
62 |
63 | stage_number: int
64 | "stage number of blade row"
65 |
66 | stage_flow_station: FlowStation
67 | "blade stage flow station (FlowStation)"
68 |
69 | vortex: Vortex
70 | "blade vortex calculation for stagger angles"
71 |
72 | AR: float
73 | "aspect ratio (dimensionless)"
74 |
75 | sc: float
76 | "spacing to chord ratio (dimensionless)"
77 |
78 | tbc: float
79 | "max thickness to chord (dimensionless)"
80 |
81 | is_rotating: bool
82 | "whether blade is rotating or not"
83 |
84 | N_stream: int
85 | "number of streams per blade (dimensionless)"
86 |
87 | metal_angle_method: MetalAngleMethods
88 | "metal angle method"
89 |
90 | next_stage_flow_station: Optional["FlowStation"] = None
91 | "next blade row flow station"
92 |
93 | deviation_iterations: int = 20
94 | "nominal deviation iterations"
95 |
96 | def __post_init__(self):
97 | assert self.N_stream % 2 != 0, "N_stream must be an odd number"
98 | if self.is_rotating and self.next_stage_flow_station is None:
99 | self.next_stage_flow_station = self.stage_flow_station.copyStream(
100 | alpha=self.vortex.alpha(self.radii, is_rotating=False),
101 | radius=self.radii
102 | )
103 |
104 | @cached_property
105 | def rt(self):
106 | "blade tip radius (m)"
107 | rt = self.stage_flow_station.outer_radius
108 | assert isinstance(rt, float)
109 | return rt
110 |
111 | @cached_property
112 | def rh(self):
113 | "blade hub radius (m)"
114 | rh = self.stage_flow_station.inner_radius
115 | assert isinstance(rh, float)
116 | return rh
117 |
118 | @cached_property
119 | def rm(self):
120 | "blade mean radius (m)"
121 | rm = self.stage_flow_station.radius
122 | assert isinstance(rm, float)
123 | return rm
124 |
125 | @cached_property
126 | def h(self):
127 | "height of blade (m)"
128 | return self.rt-self.rh
129 |
130 | @cached_property
131 | def h_disk(self):
132 | "disk height of blade row (m)"
133 | xi = self.metal_angles.xi[0] if self.is_rotating else self.metal_angles.xi[-1]
134 | return np.abs(self.c*np.cos(xi) * 1.25)
135 |
136 | @cached_property
137 | def c(self):
138 | "chord length (m)"
139 | return self.h/self.AR
140 |
141 | @cached_property
142 | def tb(self):
143 | "blade max thickness (m)"
144 | return self.tbc * self.c
145 |
146 | @cached_property
147 | def Z(self):
148 | "number of blades in row (dimensionless)"
149 | Z = np.ceil(2*np.pi*self.rm/(self.sc*self.c))
150 | if not self.is_rotating and not Z % 2 == 0:
151 | Z -= 1
152 | return int(Z)
153 |
154 | @cached_property
155 | def s(self):
156 | "spacing between blades (m)"
157 | return 2*np.pi*self.rh/self.Z
158 |
159 | @cached_property
160 | def sh(self):
161 | "spacing to height (dimensionless)"
162 | return self.s/self.h
163 |
164 | @cached_property
165 | def sigma(self):
166 | "spacing between blades (dimensionless)"
167 | return 1 / self.sc
168 |
169 | @cached_property
170 | def deHaller(self):
171 | "deHaller factor (dimensionless)"
172 | return self.next_flow_station.W / self.flow_station.W
173 |
174 | @cached_property
175 | def Re(self):
176 | "Reynold's number of blade chord (dimensionless)"
177 | return self.stage_flow_station.rho * self.stage_flow_station.Vm * (self.c / self.stage_flow_station.mu)
178 |
179 | @cached_property
180 | def airfoil_type(self):
181 | # if self.stage_flow_station.MN < 0.7:
182 | # return AirfoilType.NACA65
183 | # elif self.stage_flow_station.MN >= 0.7 and self.stage_flow_station.MN <= 1.20:
184 | # return AirfoilType.DCA
185 | # raise ValueError("MN > 1.20 not currently supported")
186 |
187 | # TODO: only have support of DCA airfoil generation at the moment
188 | return AirfoilType.DCA
189 |
190 | @cached_property
191 | def metal_angles(self):
192 | if self.metal_angle_method == "JohnsenBullock":
193 | # beta1_rm: float = np.median(self.beta1) # type: ignore
194 | # beta2_rm: float = np.median(self.beta2) # type: ignore
195 | method_angle_method = JohnsenBullockMetalAngleMethod(self.beta1, self.beta2, self.sigma, self.tbc, self.airfoil_type)
196 | metal_angle_offset = method_angle_method.get_metal_angle_offset(self.deviation_iterations)
197 | return MetalAngles(self.beta1, self.beta2, metal_angle_offset.i, metal_angle_offset.delta)
198 | return MetalAngles(self.beta1, self.beta2, 0, 0)
199 |
200 | @cached_property
201 | def radii(self):
202 | "blade radii (m)"
203 | return np.linspace(self.rh, self.rt, self.N_stream, endpoint=True)
204 |
205 | @cached_property
206 | def flow_station(self):
207 | "flow station (FlowStation)"
208 | return self.stage_flow_station.copyStream(
209 | alpha=self.vortex.alpha(self.radii, self.is_rotating),
210 | radius=self.radii
211 | )
212 |
213 | @cached_property
214 | def next_flow_station(self):
215 | "next flow station (FlowStation)"
216 | assert self.next_stage_flow_station is not None
217 | return self.next_stage_flow_station.copyStream(
218 | alpha=self.vortex.alpha(self.radii, self.is_rotating),
219 | radius=self.radii
220 | )
221 |
222 | @cached_property
223 | def beta1(self):
224 | "blade inlet flow angle (rad)"
225 | if self.is_rotating:
226 | return self.flow_station.beta # beta1
227 | return self.flow_station.alpha # alpha2
228 |
229 | @cached_property
230 | def beta2(self):
231 | "blade outlet flow angle (rad)"
232 | if self.is_rotating:
233 | assert self.next_stage_flow_station is not None
234 | return self.next_stage_flow_station.beta # beta2
235 |
236 | assert self.next_stage_flow_station is not None or self.vortex.Rm == 0.5, "next_flow_station needs to be defined or Rc=0.5"
237 | if self.next_stage_flow_station is not None:
238 | return self.next_stage_flow_station.alpha # alpha3
239 | return self.vortex.alpha(self.radii, is_rotating=not self.is_rotating) # alpha3
240 |
241 | @cached_property
242 | def DF(self):
243 | "diffusion factor (dimensionless)"
244 | return 1-(np.cos(self.beta1)/np.cos(self.beta2))+(np.cos(self.beta1)/2)*self.sigma*(np.tan(self.beta1)-np.tan(self.beta2))
245 |
246 | @cached_property
247 | def airfoils(self):
248 | r0 = self.tb * 0.15
249 | # TODO: optimize this with Numba
250 | return [
251 | DCAAirfoil(self.c, self.metal_angles.theta[i], r0, self.tb, self.metal_angles.xi[i])
252 | for i in range(self.N_stream)
253 | ]
254 |
255 | @cached_property
256 | def attachment(self):
257 | max_length = 0.75*self.s if self.is_rotating else 1*self.s
258 | attachment = FirtreeAttachment(
259 | gamma=np.radians(40),
260 | beta=np.radians(40),
261 | ll=0.15*self.s,
262 | lu=0.2*self.s,
263 | Ri=0.05*self.s,
264 | Ro=0.025*self.s,
265 | R_dove=0.05*self.s,
266 | max_length=max_length,
267 | num_stages=2,
268 | disk_radius=self.rh,
269 | tolerance=0.0006, # m, 0.5 mm
270 | include_top_arc=self.is_rotating
271 | )
272 | return attachment
273 |
274 | def to_cad_export(self):
275 | return BladeRowCadExport(
276 | stage_number=self.stage_number,
277 | disk_height=self.h_disk * MM,
278 | hub_radius=self.rh * MM,
279 | tip_radius=self.rt * MM,
280 | radii=self.radii * MM,
281 | airfoils=np.array([airfoil.get_coords() for airfoil in self.airfoils]) * MM,
282 | attachment=self.attachment.coords * MM,
283 | attachment_with_tolerance=self.attachment.coords_with_tolerance * MM,
284 | attachment_height=self.attachment.height * MM,
285 | attachment_bottom_width=self.attachment.bottom_width * MM,
286 | number_of_blades=self.Z,
287 | twist_angle=np.degrees(self.metal_angles.xi[-1]-self.metal_angles.xi[0]),
288 | is_rotating=self.is_rotating,
289 | )
290 |
--------------------------------------------------------------------------------
/turbodesigner/cad/shaft.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass, field
2 | from functools import cached_property
3 | from typing import Optional
4 | import cadquery as cq
5 | from turbodesigner.cad.common import ExtendedWorkplane, FastenerPredicter
6 | from turbodesigner.cad.blade import BladeCadModel, BladeCadModelSpecification
7 | from turbodesigner.stage import StageCadExport
8 | from turbodesigner.turbomachinery import TurbomachineryCadExport
9 |
10 |
11 | @dataclass
12 | class ShaftCadModelSpecification:
13 | is_simple: bool = False
14 | "whether to include attachment or fuse into shaft (bool)"
15 |
16 | stage_connect_length_to_heatset_thickness: float = 2.00
17 | "shaft stage connect to hub radius (dimensionless)"
18 |
19 | stage_connect_height_to_screw_head_diameter: float = 1.75
20 | "shaft stage connect to disk height (dimensionless)"
21 |
22 | stage_connect_padding_to_attachment_height: float = 1.25
23 | "shaft stage connect padding to attachment height (dimensionless)"
24 |
25 | stage_connect_heatset_diameter_to_disk_height: float = 0.05
26 | "shaft stage connect heatset diameter to disk height (dimensionless)"
27 |
28 | stage_connect_screw_quantity: int = 4
29 | "shaft stage connect screw quantity (dimensionless)"
30 |
31 | stage_connect_clearance: float = 0.5
32 | "shaft stage connect circular clearance (mm)"
33 |
34 |
35 | @dataclass
36 | class ShaftCadModel:
37 | stage: StageCadExport
38 | "turbomachinery stage"
39 |
40 | next_stage: Optional[StageCadExport] = None
41 | "next turbomachinery stage"
42 |
43 | spec: ShaftCadModelSpecification = field(default_factory=ShaftCadModelSpecification)
44 | "shaft cad model specification"
45 |
46 | def __post_init__(self):
47 | self.stage_connect_heatset = FastenerPredicter.predict_heatset(
48 | target_diameter=self.stage.rotor.disk_height*self.spec.stage_connect_heatset_diameter_to_disk_height,
49 | )
50 | self.stage_connect_length = self.stage_connect_heatset.nut_thickness * self.spec.stage_connect_length_to_heatset_thickness
51 |
52 | self.blade_cad_model = BladeCadModel(
53 | self.stage.rotor,
54 | spec=BladeCadModelSpecification(
55 | not self.spec.is_simple,
56 | screw_length_padding=self.stage_connect_length
57 | )
58 | )
59 |
60 | # TODO: Just used to get head diameter, length doesn't matter
61 | self.stage_connect_screw = FastenerPredicter.predict_screw(target_diameter=self.stage_connect_heatset.thread_diameter)
62 |
63 | self.stage_connect_height = self.stage_connect_screw.head_diameter * self.spec.stage_connect_height_to_screw_head_diameter
64 | stage_connect_padding = self.stage.rotor.attachment_height * self.spec.stage_connect_padding_to_attachment_height
65 | self.stage_connect_outer_radius = self.stage.rotor.hub_radius-stage_connect_padding
66 | self.stage_connect_inner_radius = self.stage_connect_outer_radius-self.stage_connect_length
67 |
68 | if self.next_stage:
69 | self.next_stage_shaft_cad_model = ShaftCadModel(self.next_stage, spec=self.spec)
70 | self.next_stage_stage_connect_screw = FastenerPredicter.predict_screw(
71 | target_diameter=self.next_stage_shaft_cad_model.stage_connect_heatset.thread_diameter,
72 | target_length=self.next_stage_shaft_cad_model.stage_connect_heatset.nut_thickness + (self.stage.stator.hub_radius - self.next_stage_shaft_cad_model.stage_connect_outer_radius)
73 | )
74 |
75 | @cached_property
76 | def shaft_stage_sector(self):
77 | sector_angle = 360 / self.stage.rotor.number_of_blades
78 | sector_cut_profile = (
79 | cq.Workplane('XZ')
80 | .transformed(rotate=(0, sector_angle/2, 0))
81 | .rect(self.stage.stator.hub_radius, self.stage.stage_height*2, centered=False)
82 | .revolve(sector_angle*(self.stage.rotor.number_of_blades-1), (0, 0, 0), (0, 1, 0))
83 | )
84 |
85 | shaft_profile = self.shaft_stage_assembly.objects["Stage Shaft"].obj
86 | assert shaft_profile is not None and isinstance(shaft_profile, cq.Workplane)
87 | shaft_sector_profile = (
88 | shaft_profile
89 | .cut(sector_cut_profile)
90 | )
91 |
92 | return shaft_sector_profile
93 |
94 | @cached_property
95 | def shaft_stage_assembly(self):
96 | base_assembly = cq.Assembly()
97 | blade_assembly = cq.Assembly()
98 | fastener_assembly = cq.Assembly()
99 |
100 | shaft_profile = (
101 | ExtendedWorkplane("XY")
102 | # Stator Disk
103 | .circle(self.stage.stator.hub_radius)
104 | .extrude(self.stage.stator.disk_height+self.stage.stage_gap)
105 |
106 | # Row Gap Transition Disk
107 | .faces(">Z")
108 | .workplane()
109 | .truncated_cone(
110 | start_radius=self.stage.stator.hub_radius,
111 | end_radius=self.stage.rotor.hub_radius,
112 | height=self.stage.row_gap
113 | )
114 |
115 | # Rotor Disk
116 | .faces(">Z")
117 | .workplane()
118 | .circle(self.stage.rotor.hub_radius)
119 | .extrude(self.stage.rotor.disk_height)
120 | )
121 |
122 | if not self.spec.is_simple:
123 | shaft_profile = (
124 | shaft_profile
125 |
126 | # Cut Attachments - TODO: make this operation faster
127 | .faces(">Z")
128 | .workplane()
129 | .polarArray(1.0001*self.stage.rotor.hub_radius, 0, 360, self.stage.rotor.number_of_blades)
130 | .eachpoint(
131 | lambda loc: (
132 | cq.Workplane("XY")
133 | .polyline(self.stage.rotor.attachment_with_tolerance) # type: ignore
134 | .close()
135 | .rotate((0, 0, 0), (0, 0, 1), 270)
136 | ).val().located(loc), True) # type: ignore
137 | .cutBlind(-self.stage.rotor.disk_height)
138 |
139 | # Shaft Male Connect
140 | .faces(">Z")
141 | .workplane()
142 | .circle(self.stage_connect_outer_radius)
143 | .extrude(self.stage_connect_height)
144 |
145 | # Shaft Connect Hole
146 | .faces(">Z")
147 | .workplane()
148 | .circle(self.stage_connect_inner_radius*1.001)
149 | .cutThruAll()
150 |
151 | # Blade Lock Screws
152 | .faces(">Z")
153 | .workplane(offset=-self.stage_connect_height-self.blade_cad_model.lock_screw.head_diameter*1.5)
154 | .polarArray(self.stage_connect_inner_radius, 0, 360, self.stage.rotor.number_of_blades)
155 | .mutatePoints(lambda loc: loc * cq.Location(cq.Vector(0, 0, 0), cq.Vector(0, 1, 0), -90))
156 | .clearanceHole(self.blade_cad_model.lock_screw, fit="Loose", baseAssembly=fastener_assembly)
157 |
158 | # Shaft Connect Heatsets
159 | .faces(">Z")
160 | .workplane(offset=-self.stage_connect_height/2)
161 | .polarArray(self.stage_connect_outer_radius, 0, 360, self.spec.stage_connect_screw_quantity)
162 | .mutatePoints(lambda loc: loc * cq.Location(cq.Vector(0, 0, 0), cq.Vector(0, 1, 0), 90))
163 | .insertHole(self.stage_connect_heatset, fit="Loose", baseAssembly=fastener_assembly, depth=self.stage_connect_heatset.nut_thickness)
164 | )
165 | if self.next_stage_shaft_cad_model:
166 | shaft_profile = (
167 | # Next Shaft Female Connect
168 | shaft_profile
169 | .faces("Z")
108 | .workplane()
109 | .truncated_cone(
110 | start_radius=self.stage.stator.tip_radius,
111 | end_radius=self.stage.rotor.tip_radius,
112 | height=self.stage.row_gap
113 | )
114 |
115 | # Rotor Disk
116 | .faces(">Z")
117 | .workplane()
118 | .circle(self.stage.rotor.tip_radius)
119 | .extrude(self.stage.rotor.disk_height)
120 | )
121 |
122 | casing_profile = (
123 | ExtendedWorkplane("XY")
124 | .circle(self.first_stage.rotor.tip_radius + self.casing_thickness)
125 | .extrude(self.half_connect_height)
126 | )
127 |
128 | if not self.spec.is_simple:
129 | casing_profile = (
130 | casing_profile
131 |
132 | # Stage Shaft Connect
133 | .faces("Z")
197 | .workplane()
198 | .circle(self.previous_stage_casing_cad_model.stage_connect_outer_radius)
199 | .circle(self.previous_stage_casing_cad_model.stage_connect_inner_radius)
200 | .cutBlind(-self.previous_stage_casing_cad_model.stage_connect_height)
201 |
202 | # Previous Stage Shaft Connect Heatsets
203 | .faces(">Z")
204 | .workplane(offset=-self.previous_stage_casing_cad_model.stage_connect_height/2)
205 | .transformed(rotate=(0, 0, 45))
206 | .polarArray(self.previous_stage_casing_cad_model.stage_connect_inner_radius, 0, 360, self.spec.stage_connect_screw_quantity)
207 | .mutatePoints(lambda loc: loc * cq.Location(cq.Vector(0, 0, 0), cq.Vector(0, 1, 0), 90))
208 | .insertHole(self.previous_stage_casing_cad_model.stage_connect_heatset, fit="Loose", baseAssembly=fastener_assembly, depth=self.previous_stage_casing_cad_model.stage_connect_heatset.nut_thickness)
209 | )
210 |
211 | if not self.spec.is_simple:
212 | left_casing_profile = (
213 | casing_profile
214 | .transformed(rotate=(90, -45, 0))
215 | .split(keepBottom=True)
216 | .faces("