diff --git a/docs/MIGRATION_MARIMO_Sphinx.md b/docs/MIGRATION_MARIMO_Sphinx.md new file mode 100644 index 000000000..2f87459dd --- /dev/null +++ b/docs/MIGRATION_MARIMO_Sphinx.md @@ -0,0 +1,411 @@ +# Migração de Notebooks Jupyter (.ipynb) para Marimo com Integração Sphinx + +Este documento detalha o processo passo a passo para substituir os notebooks `.ipynb` existentes em `docs/user_guide` pela biblioteca [marimo](https://marimo.io) e integrá-los à documentação Sphinx do projeto ROSS. + +--- + +## Índice + +1. [Visão Geral](#1-visão-geral) +2. [Pré-requisitos](#2-pré-requisitos) +3. [Etapa 1: Instalação](#3-etapa-1-instalação) +4. [Etapa 2: Conversão dos Notebooks](#4-etapa-2-conversão-dos-notebooks) +5. [Etapa 3: Configuração do Sphinx](#5-etapa-3-configuração-do-sphinx) +6. [Etapa 4: Adaptação dos Documentos](#6-etapa-4-adaptação-dos-documentos) +7. [Etapa 5: Reorganização do User Guide](#7-etapa-5-reorganização-do-user-guide) +8. [Etapa 6: Validação e Testes](#8-etapa-6-validação-e-testes) +9. [Etapa 7: Considerações sobre Dependências](#9-etapa-7-considerações-sobre-dependências) +10. [Referências](#10-referências) + +--- + +## 1. Visão Geral + +### Situação Atual + +- **Documentação:** Sphinx com extensão `myst-nb` +- **Notebooks:** 47 arquivos `.ipynb` em `docs/user_guide` +- **Formato:** MyST Markdown com `{toctree}` referenciando notebooks diretamente + +### Situação Desejada + +- **Notebooks:** Arquivos `.py` no formato marimo +- **Integração:** Extensão `sphinx-marimo` para embedar notebooks interativos (WASM) +- **Formato:** Diretivas RST `.. marimo::` para incorporar os notebooks na documentação + +### Diferenças Importantes + +| Aspecto | Jupyter (.ipynb) | Marimo (.py) | +|---------|------------------|--------------| +| Formato | JSON | Python puro | +| Execução | Células independentes | Reativa (grafo de dependências) | +| Outputs | Persistidos no arquivo | Calculados em tempo de execução | +| Versionamento | Difícil (JSON) | Mais simples (texto) | + +--- + +## 2. Pré-requisitos + +- **Python:** 3.8 ou superior +- **Sphinx:** 4.0 ou superior +- **Marimo:** 0.1.0 ou superior +- **Ambiente:** Virtual environment ativado com dependências do ROSS instaladas + +--- + +## 3. Etapa 1: Instalação + +### 3.1. Instalar marimo + +```bash +pip install marimo +``` + +### 3.2. Instalar sphinx-marimo + +```bash +pip install sphinx-marimo +``` + +### 3.3. Atualizar requirements.txt da documentação + +Adicionar ao arquivo `docs/requirements.txt`: + +``` +marimo +sphinx-marimo +``` + +### 3.4. Verificar instalação + +```bash +marimo --version +python -c "import sphinx_marimo; print('sphinx-marimo OK')" +``` + +--- + +## 4. Etapa 2: Conversão dos Notebooks + +### 4.1. Comando de conversão individual + +Para converter um único notebook: + +```bash +cd docs/user_guide +marimo convert example_1.ipynb -o example_1.py +``` + +**Parâmetros:** +- `example_1.ipynb` — arquivo de entrada (Jupyter notebook) +- `-o example_1.py` — arquivo de saída (notebook marimo) + +### 4.2. Conversão em lote — PowerShell (Windows) + +```powershell +cd docs/user_guide +Get-ChildItem -Filter "*.ipynb" | ForEach-Object { + $outputName = $_.BaseName + ".py" + marimo convert $_.Name -o $outputName + Write-Host "Convertido: $($_.Name) -> $outputName" +} +``` + +### 4.3. Conversão em lote — Bash (Linux/macOS) + +```bash +cd docs/user_guide +for f in *.ipynb; do + marimo convert "$f" -o "${f%.ipynb}.py" + echo "Convertido: $f -> ${f%.ipynb}.py" +done +``` + +### 4.4. Conversão silenciosa (CI/scripts) + +```bash +marimo -q -y convert notebook.ipynb -o notebook.py +``` + +- `-q` — quiet (suprime output) +- `-y` — yes (aceita prompts automaticamente) + +### 4.5. Comportamento da conversão + +- **Outputs:** São removidos durante a conversão (marimo recalcula em tempo de execução) +- **Estrutura:** Células de código viram funções decoradas com `@app.cell` +- **Markdown:** Células markdown são convertidas em strings/`mo.md()` + +### 4.6. Atenção à execução reativa + +Marimo usa **execução reativa** — quando uma variável muda, todas as células que dependem dela são reexecutadas. Código que: + +- Modifica variáveis em múltiplas células +- Usa estado mutável compartilhado +- Depende de ordem de execução manual + +pode precisar de refatoração após a conversão. + +--- + +## 5. Etapa 3: Configuração do Sphinx + +### 5.1. Editar conf.py — Adicionar extensão + +No arquivo `docs/conf.py`, na lista `extensions`: + +```python +extensions = [ + "sphinx.ext.autodoc", + "sphinx_marimo", # Nova extensão + # "myst_nb", # Remover se não for mais usar .ipynb + "sphinxcontrib.bibtex", + "sphinx_copybutton", + "sphinx.ext.autosummary", + "sphinx.ext.mathjax", + "sphinx_design", + "numpydoc", + "sphinxcontrib.googleanalytics", +] +``` + +### 5.2. Configurações do sphinx-marimo + +Adicionar após as extensões: + +```python +# --- Configuração sphinx-marimo --- +marimo_notebook_dir = "user_guide" # Diretório dos notebooks .py +marimo_default_height = "600px" # Altura padrão do iframe +marimo_default_width = "100%" # Largura padrão do iframe +marimo_click_to_load = "compact" # "compact", "overlay" ou False +marimo_load_button_text = "Carregar Notebook Interativo" +``` + +### 5.3. Opções de marimo_click_to_load + +| Valor | Descrição | +|-------|-----------| +| `False` | Notebooks carregam imediatamente (pode impactar performance) | +| `True` ou `"overlay"` | Overlay em tela cheia com botão centralizado | +| `"compact"` | Botão compacto que expande ao clicar (recomendado para vários notebooks) | + +### 5.4. Ajustar source_suffix (opcional) + +Se todos os `.ipynb` forem migrados, remover do `source_suffix`: + +```python +source_suffix = [".rst", ".md"] # Remove .ipynb +``` + +Manter `.ipynb` se houver documentos híbridos durante a transição. + +--- + +## 6. Etapa 4: Adaptação dos Documentos + +### 6.1. Sintaxe da diretiva marimo + +Em arquivos RST (`.rst`): + +```rst +.. marimo:: user_guide/example_1.py + :height: 700px + :width: 100% + :click-to-load: compact + :load-button-text: Executar Exemplo 1 +``` + +### 6.2. Opções da diretiva + +| Opção | Descrição | Padrão | +|-------|-----------|--------| +| `:height:` | Altura do iframe | `marimo_default_height` | +| `:width:` | Largura do iframe | `marimo_default_width` | +| `:click-to-load:` | Modo de carregamento (compact/overlay/false) | Config global | +| `:load-button-text:` | Texto do botão | Config global | + +### 6.3. Usar marimo em MyST Markdown + +Se precisar manter `.md`, inclua RST via fenced block: + +````markdown +# Exemplo 1 + +```{rst} +.. marimo:: user_guide/example_1.py + :height: 600px +``` +```` + +Requer que `myst_nb` ou extensão MyST suporte `{rst}`. + +--- + +## 7. Etapa 5: Reorganização do User Guide + +### 7.1. Estrutura atual (user_guide.md) + +```markdown +```{toctree} +:maxdepth: 1 +:caption: Tutorials +tutorial_part_1 +tutorial_part_2_1 +... +``` +``` + +### 7.2. Estratégia A: Arquivos RST individuais + +Criar um `.rst` por notebook, por exemplo `docs/user_guide/tutorial_part_1.rst`: + +```rst +Tutorial Parte 1 +================ + +.. marimo:: user_guide/tutorial_part_1.py + :height: 700px + :click-to-load: compact +``` + +E no `user_guide.rst` principal: + +```rst +User Guide +========== + +.. toctree:: + :maxdepth: 1 + :caption: Tutorials + + user_guide/tutorial_part_1 + user_guide/tutorial_part_2_1 + user_guide/tutorial_part_2_2 + user_guide/tutorial_part_3 + user_guide/tutorial_part_4 + user_guide/tutorial_part_5 + +.. toctree:: + :maxdepth: 1 + :caption: Examples + + user_guide/example_1 + user_guide/example_2 + ... +``` + +### 7.3. Estratégia B: Página única com seções + +Um único arquivo com todas as diretivas: + +```rst +User Guide +========== + +Tutorials +--------- + +.. marimo:: user_guide/tutorial_part_1.py +.. marimo:: user_guide/tutorial_part_2_1.py +... + +Examples +-------- + +.. marimo:: user_guide/example_1.py +.. marimo:: user_guide/example_2.py +... +``` + +### 7.4. Ordem recomendada de migração + +1. Tutoriais (tutorial_part_1 a tutorial_part_5) +2. Exemplos simples (example_1 a example_10) +3. Exemplos restantes (example_11 a example_32) +4. Fluid Flow (fluid_flow_*.ipynb) + +--- + +## 8. Etapa 6: Validação e Testes + +### 8.1. Validar notebooks convertidos + +Abrir cada notebook no editor marimo: + +```bash +marimo edit docs/user_guide/example_1.py +``` + +Verificar: +- Código executa sem erros +- Gráficos/visualizações aparecem +- Não há dependências circulares + +### 8.2. Verificar sintaxe marimo + +```bash +marimo check docs/user_guide/*.py +marimo check --fix docs/user_guide/*.py # Corrigir automaticamente +``` + +### 8.3. Build da documentação + +```bash +cd docs +make html +``` + +Ou no Windows: + +```bash +cd docs +.\make.bat html +``` + +### 8.4. Inspecionar output + +Abrir `docs/_build/html/index.html` e navegar até os notebooks. Verificar: +- Notebooks carregam (ou botão aparece) +- Layout está correto +- Interatividade funciona (se aplicável) + +--- + +## 9. Etapa 7: Considerações sobre Dependências + +### 9.1. WASM e Pyodide + +O sphinx-marimo exporta notebooks para **HTML/WASM** rodando no browser via Pyodide. Nem todos os pacotes Python são compatíveis: + +- **ross:** Provavelmente não suportado (NumPy/SciPy customizados) +- **numpy:** Parcialmente suportado +- **scipy:** Parcialmente suportado +- **plotly:** Suportado + +### 9.2. Opções de estratégia + +| Estratégia | Prós | Contras | +|------------|------|---------| +| **WASM (padrão)** | Execução no browser, sem backend | Dependências limitadas | +| **Execução estática** | Usar myst-nb para .ipynb executados | Perde interatividade | +| **Híbrido** | Exemplos simples em marimo, complexos em myst-nb | Mais complexo de manter | +| **Servidor** | Notebooks rodam via marimo run em servidor | Exige infraestrutura | + +### 9.3. Para o ROSS + +Considerar manter **myst-nb** para notebooks que usam ross, numpy avançado ou scipy, e usar **sphinx-marimo** apenas para exemplos mais simples ou tutoriais conceituais que usem bibliotecas compatíveis com Pyodide. + +--- + +## 10. Referências + +- [Marimo — Documentação Oficial](https://docs.marimo.io/) +- [Marimo CLI — Comando convert](https://docs.marimo.io/cli) +- [sphinx-marimo — GitHub](https://github.com/koaning/sphinx-marimo) +- [Marimo — Publicação com MkDocs](https://docs.marimo.io/guides/publishing/mkdocs) +- [Pyodide — Pacotes disponíveis](https://pyodide.org/en/stable/usage/packages-in-pyodide.html) + +--- + +*Documento criado para o projeto ROSS — Rotordynamic Open-Source Software* diff --git a/docs/Makefile b/docs/Makefile index 22e5c8046..91aed19d2 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,18 +1,36 @@ # Minimal makefile for Sphinx documentation +# Preferência: uv (recomendado pelo marimo) > venv > sphinx-build do PATH # -# You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = sphinx-build -SPHINXPROJ = ross SOURCEDIR = . BUILDDIR = _build +SPHINXPROJ = ross + +# Detectar uv (recomendado pelo marimo: https://docs.marimo.io/guides/package_management/using_uv/) +UV_AVAILABLE = $(shell command -v uv 2>/dev/null && echo 1) +# Fallback: venv do projeto +VENV_BIN = $(if $(wildcard ../venv/bin/sphinx-build),../venv/bin/,) +# Comando de build: uv run usa o ambiente do projeto (deps instaladas com uv pip install -r docs/requirements.txt) +ifeq ($(UV_AVAILABLE),1) + SPHINXBUILD = uv run --project .. sphinx-build +else + SPHINXBUILD = $(VENV_BIN)sphinx-build +endif + +# Com venv (sem uv), marimo no PATH para sphinx-marimo +export PATH := $(if $(VENV_BIN),$(abspath $(VENV_BIN)):$(PATH),$(PATH)) + +# Instalar deps da documentação com uv (recomendado pelo marimo) e aplicar patch --show-code +install-deps: + cd .. && uv pip install -e . && uv pip install -r docs/requirements.txt + cd .. && uv run python docs/tools/patch_sphinx_marimo_show_code.py # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -.PHONY: help Makefile +.PHONY: help Makefile install-deps # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). diff --git a/docs/conf.py b/docs/conf.py index 27ffe0671..2a2183a50 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -46,6 +46,7 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx_marimo", "myst_nb", "sphinxcontrib.bibtex", "sphinx_copybutton", @@ -75,6 +76,13 @@ # source_suffix = [".rst", ".md", ".ipynb"] +# --- Configuração sphinx-marimo --- +marimo_notebook_dir = "user_guide" +marimo_default_height = "600px" +marimo_default_width = "100%" +marimo_click_to_load = False # Carrega o notebook ao abrir a página (sem botão) +marimo_load_button_text = "Carregar Notebook Interativo" + # The master toctree document. master_doc = "index" @@ -92,7 +100,12 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +exclude_patterns = [ + "_build", + "Thumbs.db", + ".DS_Store", + "user_guide/*.ipynb", # Migrados para marimo (.py + .rst) +] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "abap" diff --git a/docs/requirements.txt b/docs/requirements.txt index 9038e4f40..36b9e59fa 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,7 @@ -Sphinx==8.2.3 +Sphinx>=7.4,<8.2 myst-nb==1.3.0 +marimo +sphinx-marimo sphinxcontrib-bibtex==2.6.5 sphinx-copybutton==0.5.2 numpydoc==1.9.0 diff --git a/docs/tools/patch_sphinx_marimo_show_code.py b/docs/tools/patch_sphinx_marimo_show_code.py new file mode 100644 index 000000000..27a433d29 --- /dev/null +++ b/docs/tools/patch_sphinx_marimo_show_code.py @@ -0,0 +1,38 @@ +""" +Aplica patch no sphinx-marimo para usar --show-code no export html-wasm, +exibindo o código nos notebooks embutidos na documentação. + +Execute após: pip install sphinx-marimo +Uso: python docs/tools/patch_sphinx_marimo_show_code.py +""" +import site +from pathlib import Path + +try: + import sphinx_marimo +except ImportError: + print("sphinx-marimo não instalado. Execute: pip install sphinx-marimo") + raise SystemExit(1) + +builder_path = Path(sphinx_marimo.__file__).parent / "builder.py" +text = builder_path.read_text() + +# Formato original (uma linha) do sphinx-marimo +old_one_line = '["marimo", "export", "html-wasm", str(notebook_path), "-o", str(output_path), "--force"]' +# Formato com quebra de linha +old_multiline = '"--force"],' +new_multiline = '"--force", "--show-code"],' +already_patched = "--show-code" in text and "html-wasm" in text + +if already_patched: + print("Patch já aplicado em", builder_path) +elif old_one_line in text: + new_one_line = old_one_line.replace('"--force"]', '"--force", "--show-code"]') + builder_path.write_text(text.replace(old_one_line, new_one_line)) + print("Patch aplicado em", builder_path) +elif old_multiline in text: + builder_path.write_text(text.replace(old_multiline, new_multiline)) + print("Patch aplicado em", builder_path) +else: + print("Não foi possível encontrar o trecho esperado em", builder_path) + raise SystemExit(1) diff --git a/docs/user_guide/__marimo__/session/tutorial_part_1.py.json b/docs/user_guide/__marimo__/session/tutorial_part_1.py.json new file mode 100644 index 000000000..864c00b73 --- /dev/null +++ b/docs/user_guide/__marimo__/session/tutorial_part_1.py.json @@ -0,0 +1,1261 @@ +{ + "version": "1", + "metadata": { + "marimo_version": "0.19.9" + }, + "cells": [ + { + "id": "MJUe", + "code_hash": "d2775288e31b8de087d4918b9a489c54", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

Tutorial - Modeling

" + } + } + ], + "console": [] + }, + { + "id": "vblA", + "code_hash": "d7d990574497a491b0c6f9e394b6591a", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "This is a basic tutorial on how to use ROSS (rotordynamics open-source software), a Python library for rotordynamic analysis. Most of this code follows object-oriented paradigm, which is represented in this\nUML DIAGRAM.\nAlso ROSS GPT, is a virtual assistant trained specifically for the ROSS package.\nBefore starting the tutorial, it is worth noting some of ROSS' design characteristics.\nFirst, we can divide the use of ROSS in two steps:\n
- Building the model;\n\n- Calculating the results.\n
\nWe can build a model by instantiating elements such as beams (shaft), disks and bearings. These elements are all defined in classes with names such as ShaftElement, BearingElement and so on.\nAfter instantiating some elements, we can then use these to build a rotor.\nThis tutorial is about building your rotor model. First, you will learn how to create and assign materials, how to instantiate the elements which compose the rotor and how to convert units in ROSS with pint library. This means that every time we call a function, we can use pint.Quantity as an argument for values that have units. If we give a float to the function ROSS will consider SI units as default.\nIn the following topics, we will discuss the most relevant classes for a quick start on how to use ROSS.
" + } + } + ], + "console": [] + }, + { + "id": "lEQa", + "code_hash": "416d79249402ed502d34bd67fc43d046", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

Section 1: Material Class

\nThere is a class called Material to hold material's properties. Materials are applied to shaft and disk elements.
" + } + } + ], + "console": [] + }, + { + "id": "PKri", + "code_hash": "d2b5419d7732eb93675270b3e8ccade6", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

1.1 Creating a material

\nTo instantiate a Material class, you only need to give 2 out of\nthe following parameters: E (Young's Modulus), G_s (Shear\nModulus) ,Poisson (Poisson Coefficient), and the material\ndensity rho.
" + } + } + ], + "console": [] + }, + { + "id": "SFPL", + "code_hash": "aaa378955f6d8e98c3c2d5b0fa1d9353", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "Note: Adding 3 arguments to the Material class raises an error." + } + } + ], + "console": [] + }, + { + "id": "BYtC", + "code_hash": "7af55142d4531737d9a1164dd8d0a821", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

1.2 Saving materials

\nTo save an already instantiated Material object, you need to use the following method.
" + } + } + ], + "console": [] + }, + { + "id": "Kclp", + "code_hash": "93173c4f791093c410d4ba73f204a841", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

1.3 Available materials

\nSaved Materials are stored in a .toml file, which can be read as .txt. The file is placed on ROSS root file with name available_materials.toml.\nIt's possible to access the Material data from the file. With the file opened, you can:\n
- modify the properties directly;\n\n- create new materials;\n
\nIt's important to keep the file structure to ensure the correct functioning of the class.\n
[Materials.Steel]\nname = "Steel"\nrho = 7810\nE = 211000000000.0\nPoisson = 0.2992610837438423\nG_s = 81200000000.0\ncolor = "#525252"\n
\nDo not change the dictionary keys and the order they're built.\nTo check what materials are available, use the command:
" + } + } + ], + "console": [] + }, + { + "id": "Hstk", + "code_hash": "58911dbc891cd3120a3dadacf964b1f5", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

1.4 Loading materials

\nAfter checking the available materials, you should use the Material.use_material('name') method with the name of the material as a parameter.
" + } + } + ], + "console": [] + }, + { + "id": "iLit", + "code_hash": "25f227091c53f5a0cff2616f45dc6d2f", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

Section 2: ShaftElement Class

\nShaftElement allows you to create cylindrical and conical shaft elements. It means you can set differents outer and inner diameters for each element node.\nThere are some ways in which you can choose the parameters to model this element:\n\nThe matrices (mass, stiffness, damping and gyroscopic) will be defined considering the following local coordinate vector:\n||([x_0, y_0, \\alpha_0, \\beta_0, x_1, y_1, \\alpha_1, \\beta_1]^T||)\nWhere\n||(\\alpha_0||) and ||(\\alpha_1||) are the bending on the yz plane\n||(\\beta_0||) and ||(\\beta_1||) are the bending on the xz plane.\nThis element represents the rotor's shaft, all the other elements are correlated with this one.
" + } + } + ], + "console": [] + }, + { + "id": "ZHCJ", + "code_hash": "a3e49cf550386baafe4352ae2b23bd05", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

2.1 Creating shaft elements

\nThe next examples present different ways of how to create a ShaftElement object, from a single element to a list of several shaft elements with different properties.\nWhen creating shaft elements, you don't necessarily need to input a specific node. If n=None, the Rotor class will assign a value to the element when building a rotor model (see section 6).\nYou can also pass the same n value to several shaft elements in the same rotor model.
" + } + } + ], + "console": [] + }, + { + "id": "ROlb", + "code_hash": "7b205f2c0cf9d4889047193a496ba7e0", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

2.1.1 Cylindrical shaft element

\nAs it's been seen, a shaft element has 4 parameters for diameters. To simplify that, when creating a cylindrical element, you only need to give 2 of them: idl and odl. So the other 2 (idr and odr) get the same values.\nNote: you can give all the 4 parameters, as long they match each other.
" + } + } + ], + "console": [] + }, + { + "id": "TqIu", + "code_hash": "53aad3276637117d431daf76a0f47111", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

2.1.2 Conical shaft element

\nTo create conical shaft elements, you must give all the 4 diameter parameters, and idl != idr and/or odl != odr.
" + } + } + ], + "console": [] + }, + { + "id": "DnEU", + "code_hash": "b2b33ed63212fc9a5ec8155ac4beabc1", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

Returning element matrices

\nUse one of this methods to return the matrices:\n
" + } + } + ], + "console": [] + }, + { + "id": "ecfG", + "code_hash": "8c859180d5d71e3e889499208d1ca6fa", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

2.1.3 List of elements - identical properties

\nNow we learned how to create elements, let's automate the process of creating multiple elements with identical properties.\nIn this example, we want 6 shaft elements with identical properties. This process can be done using a for loop or a list comprehension.
" + } + } + ], + "console": [] + }, + { + "id": "ZBYS", + "code_hash": "58c1f80effc2cb979ab281251b8b4a2a", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

2.1.4 List of elements - different properties

\nNow we learned how to create elements, let's automate the process of creating multiple elements with different properties.\nIn this example, we want 6 shaft elements which properties may not be the same. This process can be done using a for loop or a list comprehension, coupled with Python's zip() method.\nWe create lists for each property, where each term refers to a single element:
" + } + } + ], + "console": [] + }, + { + "id": "xXTn", + "code_hash": "f84afeeaff7392779cd039cb76b2c829", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

2.2 Creating shaft elements via Excel

\nThere is an option for creating a list of shaft elements via an Excel file. The classmethod .from_table() reads an Excel file created and converts it to a list of shaft elements.\nA header with the names of the columns is required. These names should match the names expected by the routine (usually the names of the parameters, but also similar ones). The program will read every row bellow the header until they end or it reaches a NaN, which means if the code reaches to an empty line, it stops iterating.\nAn example of Excel content can be found at ROSS GitHub repository at ross/tests/data/shaft_si.xls, spreadsheet \"Model\".\nYou can load it using the following code.
" + } + } + ], + "console": [] + }, + { + "id": "AjVT", + "code_hash": "ad029076da0165a22afe208f0eda82a5", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "
shaft_file = Path("shaft_si.xls")\nshaft = rs.ShaftElement.from_table(\n    file=shaft_file, sheet_type="Model", sheet_name="Model"\n)\n
" + } + } + ], + "console": [] + }, + { + "id": "pHFh", + "code_hash": "ffd0dfe99ddf4160432f33fce405f6a2", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

2.3 Creating coupling element (CouplingElement Class)

\nHere, we introduce the CouplingElement class, a subclass of the ShaftElement class, designed to model the interaction between two rotor shafts. This element is implemented in a general form in ROSS. The coupling plays a crucial role in mechanical systems by transmitting forces, vibrations, and motion between rotating shafts. It is primarily characterized by adding stiffness, mass, and inertia to the system, which are essential for simulating the real-world behavior of coupled rotors.\nSteps:\n
" + } + } + ], + "console": [] + }, + { + "id": "NCOB", + "code_hash": "789945c00e583f87c513b58607dcc695", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

2.3.1 Define the shafts with their respective dimensions and properties

" + } + } + ], + "console": [] + }, + { + "id": "TXez", + "code_hash": "ed00133296835b8a28f80518de9748e2", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

2.3.2 Define the coupling element that connects the two shafts

" + } + } + ], + "console": [] + }, + { + "id": "dNNg", + "code_hash": "0e31f8689dbb2b75f1790c1b45c57774", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "In this example, only the torsional stiffness is adopted. However, you can pass any of the following arguments (if not provided, the default value is zero):\n\nconsidering that the stiffness matrix ||(\\mathbf{K}||) will be assembled like this:\n||[\\begin{equation}\n\\mathbf{K} =\n\\begin{bmatrix}\nk_{t_x} & 0 & 0 & 0 & 0 & 0 & -k_{t_x} & 0 & 0 & 0 & 0 & 0 \\\\\n0 & k_{t_y} & 0 & 0 & 0 & 0 & 0 & -k_{t_y} & 0 & 0 & 0 & 0 \\\\\n0 & 0 & k_{t_z} & 0 & 0 & 0 & 0 & 0 & -k_{t_z} & 0 & 0 & 0 \\\\\n0 & 0 & 0 & k_{r_x} & 0 & 0 & 0 & 0 & 0 & -k_{r_x} & 0 & 0 \\\\\n0 & 0 & 0 & 0 & k_{r_y} & 0 & 0 & 0 & 0 & 0 & -k_{r_y} & 0 \\\\\n0 & 0 & 0 & 0 & 0 & k_{r_z} & 0 & 0 & 0 & 0 & 0 & -k_{r_z} \\\\\n-k_{t_x} & 0 & 0 & 0 & 0 & 0 & k_{t_x} & 0 & 0 & 0 & 0 & 0 \\\\\n0 & -k_{t_y} & 0 & 0 & 0 & 0 & 0 & k_{t_y} & 0 & 0 & 0 & 0 \\\\\n0 & 0 & -k_{t_z} & 0 & 0 & 0 & 0 & 0 & k_{t_z} & 0 & 0 & 0 \\\\\n0 & 0 & 0 & -k_{r_x} & 0 & 0 & 0 & 0 & 0 & k_{r_x} & 0 & 0 \\\\\n0 & 0 & 0 & 0 & -k_{r_y} & 0 & 0 & 0 & 0 & 0 & k_{r_y} & 0 \\\\\n0 & 0 & 0 & 0 & 0 & -k_{r_z} & 0 & 0 & 0 & 0 & 0 & k_{r_z}\n\\end{bmatrix}\n\\end{equation}||]Note: Although not demonstrated here, the same logic used for stiffness coefficients can also be applied to damping coefficients (ct_x, ct_y, ct_z, cr_x, cr_y, cr_z)." + } + } + ], + "console": [] + }, + { + "id": "wlCL", + "code_hash": "0fdae03c96cab6c2c45aac467aef11c8", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

2.3.3 Coupling all elements

\nCombine all elements into a single list for the rotor model.\nThe elements must be in order.
" + } + } + ], + "console": [] + }, + { + "id": "wAgl", + "code_hash": "23ad2f22ae5f1b4e80562bddea91af2f", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

Section 3: DiskElement Class

\nThe class DiskElement allows you to create disk elements, representing rotor equipments which can be considered only to add mass and inertia to the system, disregarding the stiffness.\nROSS offers 3 (three) ways to create a disk element:\n
    \n
  1. Inputing mass and inertia data
  2. \n
  3. Inputing geometrical and material data
  4. \n
  5. From Excel table
  6. \n
" + } + } + ], + "console": [] + }, + { + "id": "rEll", + "code_hash": "cf2f258dc945c99beea92a7eb1a1412e", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

3.1 Creating disk elements from inertia properties

\nIf you have access to the mass and inertia properties of a equipment, you can input the data directly to the element.\nDisk elements are useful to represent equipments which mass and inertia are significant, but the stiffness can be neglected.
" + } + } + ], + "console": [] + }, + { + "id": "dGlV", + "code_hash": "916d261068fcc4dbb12644fc2009df3a", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

3.1.1 Creating a single disk element

\nThis example below shows how to instantiate a disk element according to the mass and inertia properties:
" + } + } + ], + "console": [] + }, + { + "id": "lgWD", + "code_hash": "5977dbf789ea3a74aa28914918573df6", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

3.1.2 Creating a list of disk element

\nThis example below shows how to create a list of disk element according to the mass and inertia properties. The logic is the same applied to shaft elements.
" + } + } + ], + "console": [] + }, + { + "id": "LJZf", + "code_hash": "83d576155c44cea0503196261d93c431", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

3.2 Creating disk elements from geometrical properties

\nBesides the instantiation previously explained, there is a way to instantiate a DiskElement with only geometrical parameters (an approximation for cylindrical disks) and the disk\u2019s material, as we can see in the following code. In this case, there's a class method (rs.DiskElement.from_geometry()) which you can use.\nROSS will take geometrical parameters (outer and inner diameters, and width) and convert them into mass and inertia data. Once again, considering the disk as a cylinder.
" + } + } + ], + "console": [] + }, + { + "id": "urSm", + "code_hash": "0ab8877e0558b7c03e2979d7cdc0971f", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

3.2.1 Creating a single disk element

\nThis example below shows how to instantiate a disk element according to the geometrical and material properties:
" + } + } + ], + "console": [] + }, + { + "id": "mWxS", + "code_hash": "1e01078e2b030dda0d50e718b542cae6", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

3.2.2 Creating a list of disk element

\nThis example below shows how to create a list of disk element according to the geometrical and material properties. The logic is the same applied to shaft elements.
" + } + } + ], + "console": [] + }, + { + "id": "zlud", + "code_hash": "adc12b93bf8595938fc5552d5824846e", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

3.3 Creating disk elements via Excel

\nThe third option for creating disk elements is via an Excel file. The classmethod .from_table() reads an Excel file created and converts it to a list of disk elements. This method accepts only mass and inertia inputs.\nA header with the names of the columns is required. These names should match the names expected by the routine (usually the names of the parameters, but also similar ones). The program will read every row bellow the header until they end or it reaches a NaN, which means if the code reaches to an empty line, it stops iterating.\nYou can take advantage of the excel file used to assemble shaft elements, to assemble disk elements, just add a new spreadsheet to your Excel file and specify the correct sheet_name.\nAn example of Excel content can be found at diretory ross/tests/data/shaft_si.xls, spreadsheet \"More\".
" + } + } + ], + "console": [] + }, + { + "id": "xvXZ", + "code_hash": "9dfc1267c2969df4d24e6db14d498766", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

Section 4: Bearing and Seal Classes

\nROSS has a series of classes to represent elements that add stiffness and / or damping to a rotor system.\nThey're suitable to represent mainly bearings, supports and seals. Each one aims to represent some types of bearing and seal.\nAll the classes will return four stiffness coefficients (||(k_{xx}||), ||(k_{xy}||), ||(k_{yx}||), ||(k_{yy}||)) and four damping coefficients (||(c_{xx}||), ||(c_{xy}||), ||(c_{yx}||), ||(c_{yy}||)), which will be used to assemble the stiffness and damping matrices.\nThe main difference between these classes are the arguments the user must input to create the element.\nAvailable bearing classes and class methods:\n\nThe classes from item 2 to 6 inherit from BearingElement class. It means, you can use the same methods and commands, set up to BearingElement, in the other classes.
" + } + } + ], + "console": [] + }, + { + "id": "CLip", + "code_hash": "17790afd874f42e56a0f09b0099b46ef", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

4.1 BearingElement Class

\nThis class will create a bearing element. Bearings are elements that only add stiffness and damping properties to the rotor system. These parameters are defined by 8 dynamic coefficients (4 stiffness coefficients and 4 damping coefficients).\nParameters can be a constant value or speed dependent. For speed dependent parameters, each argument should be passed as an array and the correspondent speed values should also be\npassed as an array. Values for each parameter will be interpolated for the speed.\nBearing elements are single node elements and linked to \"ground\", but it's possible to create a new node with n_link argument to introduce a link with other elements. Useful to add bearings in series or co-axial rotors.
" + } + } + ], + "console": [] + }, + { + "id": "YECM", + "code_hash": "c81d9b12fb78b6ade848ab0dd918559b", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

4.1.1 Bearing with constant coefficients

\nBearings can have a constant value for each coefficient. In this case, it's not necessary to give a value to frequency argument.\nThe next example shows how to instantiate a single bearing with constant coefficients:
" + } + } + ], + "console": [] + }, + { + "id": "iXej", + "code_hash": "b93b8fd39819720950521305f2bb6451", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

4.1.2 Bearing with varying coefficients

\nThe coefficients could be an array with different values for different rotation speeds, in that case you only have to give a parameter 'frequency' which is a array with the same size as the coefficients array.\nThe next example shows how to instantiate a single bearing with speed dependent parameters:
" + } + } + ], + "console": [] + }, + { + "id": "UmEG", + "code_hash": "bb884f69e4c244d170fffd977e476871", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "If the size of coefficient and frequency arrays do not match, an ValueError is raised\nThe next example shows the instantiate of a bearing with odd parameters:" + } + } + ], + "console": [] + }, + { + "id": "vEBW", + "code_hash": "3e8e9819fb353c3bde084a1847f0b8f4", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "
bearing_odd = rs.BearingElement( # odd dimensions\n    n=0,\n    kxx=np.array([0.5e6, 1.0e6, 2.5e6]),\n    kyy=np.array([1.5e6, 2.0e6, 3.5e6]),\n    cxx=np.array([0.5e3, 1.0e3, 1.5e3]),\n    frequency=np.array([0, 1000, 2000, 3000])\n)\n
" + } + } + ], + "console": [] + }, + { + "id": "kLmu", + "code_hash": "6d7e9927fc642714467d7a84f322608d", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

4.1.3 Inserting bearing elements in series

\nBearing and seal elements are 1-node element, which means the element attaches to a given node from the rotor shaft and it's connect to the \"ground\". However, there's an option to couple multiple elements in series, using the n_link argument. This is very useful to simulate structures which support the machine, for example.\nn_link opens a new node to the rotor system, or it can be associated to another rotor node (useful in co-axial rotor models). Then, the new BearingElement node, is set equal to the n_link from the previous element.
" + } + } + ], + "console": [] + }, + { + "id": "dxZZ", + "code_hash": "e13483069e2dd794ac13ef6ac429bfa2", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

4.1.4 Visualizing coefficients graphically

\nIf you want to visualize how the coefficients varies with speed, you can select a specific coefficient and use the .plot() method.\nLet's return to the example done in 4.1.2 and check how ||(k_{yy}||) and ||(c_{yy}||) varies. You can check for all the 8 dynamic coefficients as you like.
" + } + } + ], + "console": [] + }, + { + "id": "IaQp", + "code_hash": "825da857c0f254494ee2b69b6b19fdeb", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

4.2 SealElement Class

\nSealElement class method have the exactly same arguments than BearingElement. The differences are found in some considerations when assembbling a full rotor model. For example, a SealElement won't generate reaction forces in a static analysis. So, even they are very similar when built, they have different roles in the model.\nLet's see an example:
" + } + } + ], + "console": [] + }, + { + "id": "fCoF", + "code_hash": "9852215bc041a0991a07eaca03f07f60", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

4.3 BallBearingElement Class

\nThis class will create a bearing element based on some geometric and constructive parameters of ball bearings. The main difference is that cross-coupling stiffness and damping are not modeled in this case.\nLet's see an example:
" + } + } + ], + "console": [] + }, + { + "id": "zVRe", + "code_hash": "a4eacd00448dacf8fd8875e64eeac7f8", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

4.4 RollerBearingElement Class

\nThis class will create a bearing element based on some geometric and constructive parameters of roller bearings. The main difference is that cross-coupling stiffness and damping are not modeled in this case.\nLet's see an example:
" + } + } + ], + "console": [] + }, + { + "id": "HnMC", + "code_hash": "32797eaeb3b0c4f75db2516fb6ccc2de", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

4.5 MagneticBearingElement Class

\nThis class creates a magnetic bearing element. You can input electromagnetic parameters and PID gains. ROSS converts them to stiffness and damping coefficients. To do it, use the class MagneticBearingElement()\nSee the following reference for the electromagnetic parameters g0, i0, ag, nw, alpha:\nBook: Magnetic Bearings. Theory, Design, and Application to Rotating Machinery\nAuthors: Gerhard Schweitzer and Eric H. Maslen\nPage: 84-95\nFrom: \"Magnetic Bearings. Theory, Design, and Application to Rotating Machinery\"\nAuthors: Gerhard Schweitzer and Eric H. Maslen\nPage: 354\nLet's see an example:
" + } + } + ], + "console": [] + }, + { + "id": "VCRE", + "code_hash": "b8f3af59ebd8f6a0c313db572906b8d2", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

4.6 PlainJournal Class

\nThis class computes the pressure and temperature fields within the oil film of a cylindrical bearing while also determining the stiffness and damping coefficients. These calculations are based on a wide range of inputs, including bearing geometry, operating conditions, fluid properties, turbulence modeling, and mesh discretization.\nClass arguments:\n
" + } + } + ], + "console": [] + }, + { + "id": "PSUk", + "code_hash": "aeacbac40201997517f9b3ac45ed4666", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

Plot pressure distribution

\nYou can choose the axial element on which you want to see the pressure distribution on the bearing.
" + } + } + ], + "console": [] + }, + { + "id": "vGiW", + "code_hash": "38127a0a0c66e04044d03f7bf95e5c9c", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

4.7 Creating bearing elements via Excel

\nThere's an option for creating bearing elements via an Excel file. The classmethod .from_table() reads an Excel file and converts it to a BearingElement instance. Differently from creating shaft or disk elements, this method creates only a single bearing element. To create a list of bearing elements, the user should open several spreadsheets in the Excel file and run a list comprehension loop appending each element to the list.\nA header with the names of the columns is required. These names should match the names expected by the routine (usually the names of the parameters, but also similar ones). The program will read every row below the header until they end or it reaches a NaN, which means if the code reaches an empty line, it stops iterating.\n
n : int\n    The node in which the bearing will be located in the rotor.\nfile: str\n    Path to the file containing the bearing parameters.\nsheet_name: int or str, optional\n    Position of the sheet in the file (starting from 0) or its name. If none is passed, it is\n    assumed to be the first sheet in the file.\n
\nAn example of Excel content can be found at directory ross/tests/data/bearing_seal_si.xls, spreadsheet \"XLUserKCM\".
" + } + } + ], + "console": [] + }, + { + "id": "bMrW", + "code_hash": "3b822d97027adf22ecba4d9f85ed0708", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "As .from_table() creates only a single bearing, let's see an example how to create multiple elements without typing the same command line multiple times.\n" + } + } + ], + "console": [] + }, + { + "id": "OfTS", + "code_hash": "e37c888c35d6791759713402d6dbcf0f", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

Section 5: PointMass Class

\nThe PointMass class creates a point mass element. This element can be used to link other elements in the analysis. The mass provided can be different on the x and y direction (e.g. different support inertia for x and y directions).\nPointMass also keeps the mass, stiffness, damping and gyroscopic matrices sizes consistence. When adding 2 bearing elements in series, it opens a new node with new degrees of freedom (DoF) (see section 4.1.3) and expands the stiffness and damping matrices. For this reason, it's necessary to add mass values to those DoF to match the matrices sizes.\nIf you input the argument m, the code automatically replicate the mass value for both directions \"x\" and \"y\".\nLet's see an example of creating point masses:
" + } + } + ], + "console": [] + }, + { + "id": "rSYo", + "code_hash": "49d9486a8772ee22dd00af2299c06410", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

Section 6: Rotor Class

\nRotor is the main class from ROSS. It takes as argument lists with all elements and assembles the mass, gyroscopic, damping and stiffness global matrices for the system. The object created has several methods that can be used to evaluate the dynamics of the model (they all start with the prefix .run_).\nTo use this class, you must input all the already instantiated elements in a list format.\nIf the shaft elements are not numbered, the class set a number for each one, according to the element's position in the list supplied to the rotor constructor.\nTo assemble the matrices, the Rotor class takes the local DoF's index from each element (element method .dof_mapping()) and calculate the global index
" + } + } + ], + "console": [] + }, + { + "id": "HuZB", + "code_hash": "17d5dd0bde5a46399900d8e8bf90fc5b", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

6.1 Creating a rotor model

\nLet's create a simple rotor model with ||(1.5 m||) length with 6 identical shaft elements, 2 disks, 2 bearings in the shaft ends and a support linked to the first bearing. First, we create the elements, then we input them to the Rotor class.
" + } + } + ], + "console": [] + }, + { + "id": "Ynfw", + "code_hash": "017dc9c5055217b295d610d73c3a8826", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

6.2 Adding new nodes

\nYou can add new nodes to the model based on a list of their positions by using the method .add_nodes():
" + } + } + ], + "console": [] + }, + { + "id": "uDnK", + "code_hash": "ea13886d68d71099a30932f5801549d9", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

6.3 Creating a rotor from sections

\nAn alternative to build rotor models is dividing the rotor in sections. Each section gets the same number of shaft elements.\nThere's an important difference in this class method when placing disks and bearings. The argument n will refer, not to the element node, but to the section node. So if your model has 3 sections with 4 elements each, there're 4 section nodes and 13 element nodes.\nLet's repeat the rotor model from the last example, but using .from_section() class method, without the support.
" + } + } + ], + "console": [] + }, + { + "id": "MIsd", + "code_hash": "59ff6b567ee25ebfd53a710a5def9f44", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

6.4 Visualizing the rotor model

\nIt is interesting to plot the rotor to check if the geometry checks with what you wanted to the model. Use the .plot_rotor() method to create a plot.\nnodes argument is useful when your model has lots of nodes and the visualization of nodes label may be confusing. Set an increment to the plot nodes label\nROSS uses PLOTLY as main plotting library:\nWith the Plotly, you can hover the mouse icon over the shaft, disk and point mass elements to check some of their parameters.
" + } + } + ], + "console": [] + }, + { + "id": "Lpqv", + "code_hash": "5aa7e913a8a5868dc3cb21355c0c3f2a", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "Let's visualize another rotor example with overlapping shaft elements:" + } + } + ], + "console": [] + }, + { + "id": "pCao", + "code_hash": "cd52ab3076288fdba7b0205cdc9f5f75", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

6.5 Saving a rotor model

\nYou can save a rotor model using the method .save(). This method saves the each element type and the rotor object in different .toml files.\nYou just need to input a name and the diretory, where it will be saved. If you don't input a file_path, the rotor model is saved inside the \"ross\" folder.\nTo save the rotor2 we can use:
" + } + } + ], + "console": [] + }, + { + "id": "wlyU", + "code_hash": "c6b5dd82e31dc5f1e305122f03239f1b", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

6.6 Loading a rotor model

\nYou can load a rotor model using the method .load(). This method loads a previously saved rotor model.\nYou just need to input the file path to the method.\nNow, let's load the rotor2 we saved before:
" + } + } + ], + "console": [] + }, + { + "id": "GrQN", + "code_hash": "9804f976f304d003676f64ad930be72b", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

6.7 Using CouplingElement as lumped-element

\nShafts can be connected using the CouplingElement class, which can also function as a lumped-element model.\nFor instance, a motor can be represented using a CouplingElement by specifying its stiffness, mass, and polar moment of inertia.\nHere's an example:
" + } + } + ], + "console": [] + }, + { + "id": "Ilkb", + "code_hash": "7d4b8f2dbd3d9a70ce26f91a627f0c90", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

6.7.1 Creating coupling element

" + } + } + ], + "console": [] + }, + { + "id": "vGkK", + "code_hash": "33502e1398178f4eb755cc2010c4252f", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

6.7.2 Creating motor as lumped-element

" + } + } + ], + "console": [] + }, + { + "id": "NOfw", + "code_hash": "20e44cf42ce2bac4c02340a58964bd57", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

6.7.3 Connecting coupling and motor to the end

\nSince CouplingElement is a type of ShaftElement, it must be inserted into the appropriate position within the shaft_elements array.
" + } + } + ], + "console": [] + }, + { + "id": "mSxP", + "code_hash": "b897ed744e112b82be99ac205d9b3a35", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

6.7.6 Creating coupled rotor

" + } + } + ], + "console": [] + }, + { + "id": "UFLA", + "code_hash": "19e45b775f310afdad1f99c58e9b69d5", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

Section 7: ROSS Units System

\nROSS uses an units system package called Pint.\nPint defines, operates and manipulates physical quantities: the product of a numerical value and a unit of measurement. It allows arithmetic operations between them and conversions from and to different units.\nWith Pint, it's possible to define units to every element type available in ROSS and manipulate the units when plotting graphs. ROSS takes the user-defined units and internally converts them to the International System (SI).\nImportant: It's not possible to manipulate units for attributes from any class. Attributes' values are always returned converted to SI. Only plot methods are able to manipulate the output unit.
" + } + } + ], + "console": [] + }, + { + "id": "RCUM", + "code_hash": "6a361cfbcac8720d4c371bc5b8f25cd3", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

7.1 Inserting units

\nWorking with Pint requires a specific syntax to assign an unit to an argument.\nFirst of all, it's necessary to import a function called Q_ from ross.units. This function must be assigned to every variable that are desired to have units, followed by a tuple containing the magnitude and the unit (in string format).\nThe example below shows how to create a material using Pint, and how it is returned to the user.
" + } + } + ], + "console": [] + }, + { + "id": "NBGo", + "code_hash": "0a020926a37f096001858f30143e7b89", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "Note: Taking a closer look to the output values, the material density is converted to the SI and it's returned this way to the user." + } + } + ], + "console": [] + }, + { + "id": "ChmK", + "code_hash": "fa8bd17f9293537362aef0afdd6c76ff", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "The same syntax applies to elements instantiation, if units are desired. Besides, notice the output is displayed in SI units.\n

Shaft Element using Pint

" + } + } + ], + "console": [] + }, + { + "id": "vkvV", + "code_hash": "179d8d81fc3ed78500b92827b7ec4953", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

Bearing Element using Pint

" + } + } + ], + "console": [] + }, + { + "id": "tasN", + "code_hash": "326a786de82bd7644833237b2f4ed6c5", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "

7.2 Manipulating units for plotting

\nThe plot methods presents arguments to change the units for each axis. This kind of manipulation does not affect the resulting data stored. It only converts the data on the graphs.\nThe arguments names follow a simple logic. It is the \"axis name\" underscore \"units\" (axisname_units). It should help the user to identify which axis to modify. For example:\n\nIt's not necessary to add units previously to each element or material to use Pint with plots. But keep in mind ROSS will considers results values in the SI units.\nNote: If you input data using the Imperial System, for example, without using Pint, ROSS will consider it's in SI if you try to manipulate the units when plotting.\nLet's run a simple example of manipulating units for plotting.
" + } + } + ], + "console": [] + }, + { + "id": "CFll", + "code_hash": "74acde19a770f699b4be4af1f9d3920a", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "Plotting with default options will bring graphs with SI units. X and Y axes representing the frequencies are set to rad/s" + } + } + ], + "console": [] + }, + { + "id": "viyg", + "code_hash": "c551e39faf2c387ff03a9fe3227cb67e", + "outputs": [ + { + "type": "data", + "data": { + "text/markdown": "Now, let's change the units to RPM.\nJust by adding frequency_units=\"rpm\" to plot method, you'll change the plot units." + } + } + ], + "console": [] + }, + { + "id": "Hbol", + "code_hash": "1d0db38904205bec4d6f6f6a1f6cec3e", + "outputs": [ + { + "type": "data", + "data": { + "text/plain": "" + } + } + ], + "console": [] + }, + { + "id": "bkHC", + "code_hash": "37858a3cdb80f243220cb380984c95f7", + "outputs": [ + { + "type": "error", + "ename": "exception", + "evalue": "No module named 'numpy'", + "traceback": [] + } + ], + "console": [ + { + "type": "stream", + "name": "stderr", + "text": "
Traceback (most recent call last):\n  File "/tmp/marimo_50761/__marimo__cell_bkHC_.py", line 3, in <module>\n    import numpy as np\nModuleNotFoundError: No module named 'numpy'\n
\n
", + "mimetype": "application/vnd.marimo+traceback" + } + ] + }, + { + "id": "Xref", + "code_hash": "98a9e37bf4daf37ae53307cd49517ec9", + "outputs": [], + "console": [] + }, + { + "id": "RGSE", + "code_hash": "9ba621553ed15e1292c603f11e8d72b3", + "outputs": [], + "console": [] + }, + { + "id": "emfo", + "code_hash": "51db0ad1a5eb926fe010685a1778f5b7", + "outputs": [], + "console": [] + }, + { + "id": "nWHF", + "code_hash": "23b9f1183ad8267093c2ad11694b5ef2", + "outputs": [], + "console": [] + }, + { + "id": "qnkX", + "code_hash": "86c8d2b6d5b5f8474b5144cfc9533a8f", + "outputs": [], + "console": [] + }, + { + "id": "Vxnm", + "code_hash": "b3bac4a31ff50c482299602247ce3e97", + "outputs": [], + "console": [] + }, + { + "id": "ulZA", + "code_hash": "704fe3c3ce90a7e01797c168a4825250", + "outputs": [], + "console": [] + }, + { + "id": "Pvdt", + "code_hash": "9860d6e784ba11d835f6bcd66cfba085", + "outputs": [], + "console": [] + }, + { + "id": "aLJB", + "code_hash": "58b28f9465ab83ac238b5c0da5a6d5d4", + "outputs": [], + "console": [] + }, + { + "id": "nHfw", + "code_hash": "5ebbf838a1a00bf81d9be80292031a69", + "outputs": [], + "console": [] + }, + { + "id": "aqbW", + "code_hash": "6d095fbf69e73a6d9b28b24196b7351a", + "outputs": [], + "console": [] + }, + { + "id": "TRpd", + "code_hash": "6ac7926a094445f8f4785a70cc73e169", + "outputs": [], + "console": [] + }, + { + "id": "yCnT", + "code_hash": "671da7a875990fc0c96abb0493d4e557", + "outputs": [], + "console": [] + }, + { + "id": "kqZH", + "code_hash": "56b4d2737e746112e34a815266ca8a94", + "outputs": [], + "console": [] + }, + { + "id": "SdmI", + "code_hash": "ab6e59219796caf4cf186fd2fc12b40b", + "outputs": [], + "console": [] + }, + { + "id": "yOPj", + "code_hash": "f83d27248aba62e547d46f3947e9a1c7", + "outputs": [], + "console": [] + }, + { + "id": "fwwy", + "code_hash": "92b83623fec610aec7cbdde280b37270", + "outputs": [], + "console": [] + }, + { + "id": "jxvo", + "code_hash": "3123f26d10e6c76a44d182a47bbe2462", + "outputs": [], + "console": [] + }, + { + "id": "CcZR", + "code_hash": "baffaf61ff4192b0153aab5a707d9bf9", + "outputs": [], + "console": [] + }, + { + "id": "YWSi", + "code_hash": "891c6ed9edf03dabe832508ea132cc63", + "outputs": [], + "console": [] + }, + { + "id": "tZnO", + "code_hash": "49003a6febac5608b2ff230644ce6d09", + "outputs": [], + "console": [] + }, + { + "id": "cEAS", + "code_hash": "9881b62ef15edb37de5b66ce6a7129b5", + "outputs": [], + "console": [] + }, + { + "id": "EJmg", + "code_hash": "039378f7d3066135327368acb0d1ac48", + "outputs": [], + "console": [] + }, + { + "id": "IpqN", + "code_hash": "1b1fdb0f05cad91bfae4646a6b89a6f3", + "outputs": [], + "console": [] + }, + { + "id": "dlnW", + "code_hash": "7dc0db803eaa26087c86db86922f866b", + "outputs": [], + "console": [] + }, + { + "id": "TTti", + "code_hash": "60581e93a965bd8aeb07fb5b511700de", + "outputs": [], + "console": [] + }, + { + "id": "RKFZ", + "code_hash": "8c5ec63a7d6e8bbe9f797b4d7f8e693b", + "outputs": [], + "console": [] + }, + { + "id": "IWgg", + "code_hash": "82a8b20bf8a8011fae3e32499ceec93b", + "outputs": [], + "console": [] + }, + { + "id": "LkGn", + "code_hash": "6fa3d8820bd015cf2fc56e5702f93d2b", + "outputs": [], + "console": [] + }, + { + "id": "woaO", + "code_hash": "ab98520d2702b8dd9cadf5880b1be837", + "outputs": [], + "console": [] + }, + { + "id": "wadT", + "code_hash": "8b8cf242ed3ec4be44d4561a1c0c7de8", + "outputs": [], + "console": [] + }, + { + "id": "hgqU", + "code_hash": "dae6a8e7542858baeafbfb3d517e4802", + "outputs": [], + "console": [] + }, + { + "id": "mfOT", + "code_hash": "2bbb15cc3279c988759f44dde7b59bee", + "outputs": [], + "console": [] + }, + { + "id": "SYQT", + "code_hash": "eafcd1ca657f263251bee54bb37ae275", + "outputs": [], + "console": [] + }, + { + "id": "PSQn", + "code_hash": "56ebb2a29693059062f224a37a1aabe1", + "outputs": [], + "console": [] + }, + { + "id": "lQxp", + "code_hash": "3d69ae99632dde35e22ad04d8b2d3b5a", + "outputs": [], + "console": [] + }, + { + "id": "Plbk", + "code_hash": "c2de5b3a6e9589c660da54e08e2ce803", + "outputs": [], + "console": [] + }, + { + "id": "WfYj", + "code_hash": "1c7bb708cb19a2af8029acdd60ed4bbb", + "outputs": [], + "console": [] + }, + { + "id": "LqFA", + "code_hash": "0aa87241289dd5274f05818ad61a51c8", + "outputs": [], + "console": [] + }, + { + "id": "aWBL", + "code_hash": "5248939b0ea6ee12af788d6fa59eb814", + "outputs": [], + "console": [] + }, + { + "id": "IrqS", + "code_hash": "76f84ea4b627acdd12e5dc789c9f522d", + "outputs": [], + "console": [] + }, + { + "id": "upgv", + "code_hash": "452e804f22c2b06492d4361c42bbceea", + "outputs": [], + "console": [] + }, + { + "id": "WJUG", + "code_hash": "3f81a2d5bff60243e1d1ed359397dd44", + "outputs": [], + "console": [] + }, + { + "id": "wEIy", + "code_hash": "3d8919dcf5c0d2c03126fee9737c283e", + "outputs": [], + "console": [] + }, + { + "id": "PieA", + "code_hash": "cdff615daeb54b4ceca36886fa74883f", + "outputs": [], + "console": [] + }, + { + "id": "mySd", + "code_hash": "137288b5a0ef856628ae82f7128be8df", + "outputs": [], + "console": [] + }, + { + "id": "zmIa", + "code_hash": "11839332471bf664e8552ff166331fcd", + "outputs": [], + "console": [] + }, + { + "id": "jXAc", + "code_hash": "c38756fee55b02e4ccd94e15124db1b7", + "outputs": [], + "console": [] + }, + { + "id": "IZEX", + "code_hash": "20e463d1e95211040f6c589a865130dc", + "outputs": [], + "console": [] + }, + { + "id": "ynmH", + "code_hash": "30520189e99fe82f619775f701af5ff1", + "outputs": [], + "console": [] + }, + { + "id": "eooq", + "code_hash": "c27c6c847331821fe7e9b7c04bf82ab6", + "outputs": [], + "console": [] + }, + { + "id": "KdvC", + "code_hash": "ef9744d36cfdecc1fa3bf87772c42c70", + "outputs": [], + "console": [] + }, + { + "id": "EKSN", + "code_hash": "4b9e6e9ae86ac3eac0c2634ba8043b15", + "outputs": [], + "console": [] + }, + { + "id": "SXvM", + "code_hash": "898b6929be50a4ba29fd25128e347737", + "outputs": [], + "console": [] + }, + { + "id": "wbrJ", + "code_hash": "127e2f98cb0e6a9c52b6cda9000aac63", + "outputs": [], + "console": [] + }, + { + "id": "GbuD", + "code_hash": "3e04c0b6dc341f95fb4e9678bb0429aa", + "outputs": [], + "console": [] + } + ] +} \ No newline at end of file diff --git a/docs/user_guide/example_1.py b/docs/user_guide/example_1.py new file mode 100644 index 000000000..306deb6f7 --- /dev/null +++ b/docs/user_guide/example_1.py @@ -0,0 +1,157 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell +async def _(): + """No navegador (Pyodide), instala plotly via micropip antes dos imports.""" + import sys + if "pyodide" in sys.modules: + import micropip + await micropip.install("plotly") + _packages_ready = True + return (_packages_ready,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 1 - Number of DOF influence in Natural Frequency + ========= + In this example, we use the rotor seen in Example 5.8.1 from {cite}`friswell2010dynamics`. Which is a symmetric rotor with a single disk in the center. The shaft is hollow with an outside diameter of $80 mm$, an inside + diameter of $30 mm$, and a length of $1.2 m$ and it is modeled using Euler-Bernoulli elements, with no internal shaft damping. + The bearings are rigid and short and the disk has a diameter of $400 mm$ and a thickness + of $80 mm$. + The disk and shaft elements are made of steel. + """) + return + + +@app.cell +def _(_packages_ready): + try: + import numpy as np + import plotly.graph_objects as go + import ross as rs + import plotly.io as pio + pio.renderers.default = "notebook" + _deps_ok = True + except ModuleNotFoundError: + _deps_ok = False + np = go = rs = None + return (go, np, rs, _deps_ok) + + +@app.cell +def _(mo, rs, _deps_ok): + if not _deps_ok or rs is None: + mo.md( + r""" + **Notebook no navegador (Pyodide):** A biblioteca **ROSS** não está disponível no browser. + (Plotly é instalado automaticamente via micropip.) Para rodar este exemplo com ROSS localmente use **uv** (recomendado): + `uv run marimo edit docs/user_guide/example_1.py` + """ + ) + steel = None + else: + steel = rs.materials.steel + return (steel,) + + +@app.cell +def _(): + number_of_elements = [2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 40, 60] + return (number_of_elements,) + + +@app.cell +def _(rs, steel, _deps_ok): + if not _deps_ok or rs is None or steel is None: + create_rotor = None + else: + def create_rotor(n_el): + """Create example rotor with given number of elements.""" + shaft = [ + rs.ShaftElement(1.2 / (n_el), idl=0.03, odl=0.08, material=steel) + for i in range(n_el) + ] + disks = [ + rs.DiskElement.from_geometry( + n=(n_el / 2), material=steel, width=0.08, i_d=0.08, o_d=0.4 + ) + ] + bearings = [ + rs.BearingElement(0, kxx=1e15, cxx=0), + rs.BearingElement(n_el, kxx=1e15, cxx=0), + ] + return rs.Rotor(shaft, disks, bearings) + return (create_rotor,) + + +@app.cell +def _(create_rotor, go, np, number_of_elements): + if create_rotor is None or go is None or np is None: + analysis = None + else: + def analysis(speed): + """Perform convergence analysis for a given speed.""" + n_eigen = 8 + rotor_80 = create_rotor(80) + modal_80 = rotor_80.run_modal(speed, num_modes=2 * n_eigen) + errors = np.zeros([len(number_of_elements), n_eigen]) + for i, n_el in enumerate(number_of_elements): + rotor = create_rotor(n_el) + modal = rotor.run_modal(speed, num_modes=2 * n_eigen) + errors[i, :] = abs( + 100 * (modal.wn[:n_eigen] - modal_80.wn[:n_eigen]) / modal_80.wn[:n_eigen] + ) + fig = go.Figure() + for i in range(8): + fig.add_trace( + go.Scatter(x=number_of_elements, y=errors[:, i], name=f"Mode {i}") + ) + fig.update_layout( + xaxis=dict(title="Number of degrees of freedom"), + yaxis=dict(type="log", title="Natural Frequency Error(%)"), + ) + return fig + return (analysis,) + + +@app.cell +def _(analysis): + if analysis is not None: + analysis(speed=0) + return + + +@app.cell +def _(analysis, np): + if analysis is not None and np is not None: + analysis(speed=5000 * np.pi / 30) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_1.rst b/docs/user_guide/example_1.rst new file mode 100644 index 000000000..f6e21473f --- /dev/null +++ b/docs/user_guide/example_1.rst @@ -0,0 +1,5 @@ +Example 1 +========= + +.. marimo:: example_1.py + :height: 700px diff --git a/docs/user_guide/example_10.py b/docs/user_guide/example_10.py new file mode 100644 index 000000000..b0403d28a --- /dev/null +++ b/docs/user_guide/example_10.py @@ -0,0 +1,156 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 10 - Example of rotor connected by a rigid mechanical coupling with parallel misalignment. + ===== + + In this example, we use a finite element (FE) model to represent the rotor that is available as a test rig in LMest - UFU for demonstrating the results obtained when we have a misalignment fault present in the system. + The system is composed of a flexible steel shaft with $0.914 m$ length and $0.019 m$ diameter (it is modeled using Timoshenko beam-element), two rigid discs (both of steel with $0.150 m$ diameter and $0.020 m$ thickness) and two self alignment ball bearings. + A model updating procedure was used to obtain a representative FE model for the numerical simulations presented in this work. In this sense, a heuristic optimization technique (Differential Evolution) was used to determine the unknown parameters of the model, namely the stiffness and damping coefficients of the bearings, besides the proportional damping. + """) + return + + +app._unparsable_cell( + r""" + import numpy as np + + import ross as rs + from ross.faults import * + from ross.units import Q_ + from ross.probe import Probe + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + """, + name="_" +) + + +@app.cell +def _(np, rs): + """Create example rotor with given number of elements.""" + steel2 = rs.Material(name="Steel", rho=7850, E=2.17e11, G_s=81.2e9) + # Rotor with 6 DoFs, with internal damping, with 10 shaft elements, 2 disks and 2 bearings. + i_d = 0 + o_d = 0.019 + n = 33 + + # fmt: off + L = np.array( + [0 , 25, 64, 104, 124, 143, 175, 207, 239, 271, + 303, 335, 345, 355, 380, 408, 436, 466, 496, 526, + 556, 586, 614, 647, 657, 667, 702, 737, 772, 807, + 842, 862, 881, 914] + )/ 1000 + # fmt: on + + L = [L[i] - L[i - 1] for i in range(1, len(L))] + + shaft_elem = [ + rs.ShaftElement( + material=steel2, + L=l, + idl=i_d, + odl=o_d, + idr=i_d, + odr=o_d, + alpha=8.0501, + beta=1.0e-5, + rotary_inertia=True, + shear_effects=True, + ) + for l in L + ] + + Id = 0.003844540885417 + Ip = 0.007513248437500 + + disk0 = rs.DiskElement(n=12, m=2.6375, Id=Id, Ip=Ip) + disk1 = rs.DiskElement(n=24, m=2.6375, Id=Id, Ip=Ip) + + kxx1 = 4.40e5 + kyy1 = 4.6114e5 + kzz = 0 + cxx1 = 27.4 + cyy1 = 2.505 + czz = 0 + kxx2 = 9.50e5 + kyy2 = 1.09e8 + cxx2 = 50.4 + cyy2 = 100.4553 + + bearing0 = rs.BearingElement( + n=4, kxx=kxx1, kyy=kyy1, cxx=cxx1, cyy=cyy1, kzz=kzz, czz=czz + ) + bearing1 = rs.BearingElement( + n=31, kxx=kxx2, kyy=kyy2, cxx=cxx2, cyy=cyy2, kzz=kzz, czz=czz + ) + + rotor = rs.Rotor(shaft_elem, [disk0, disk1], [bearing0, bearing1]) + return (rotor,) + + +@app.cell +def _(Probe, np): + """Inserting a mass and phase unbalance and defining the local response.""" + mass_unb = [5e-4, 0] + phase_unb = [-np.pi / 2, 0] + node_unb = [12, 24] + probe1 = Probe(14, 0) + probe2 = Probe(22, 0) + return mass_unb, node_unb, phase_unb, probe1, probe2 + + +@app.cell +def _(Q_, mass_unb, node_unb, np, phase_unb, rotor): + """Calculate the response of the rotor connected by a rigid mechanical coupling with parallel misalignment.""" + results = rotor.run_misalignment( + coupling="rigid", + mis_distance=2e-4, + input_torque=0, + load_torque=0, + n=0, + speed=Q_(1200, "RPM"), + node=node_unb, + unbalance_magnitude=mass_unb, + unbalance_phase=phase_unb, + t=np.arange(0, 5, 0.0001), + model_reduction={"num_modes": 12}, # Pseudo-modal method, optional + ) + return (results,) + + +@app.cell +def _(probe1, probe2, results): + """Plots the time response for the two given probes.""" + results.plot_1d([probe1, probe2]).show() + return + + +@app.cell +def _(Q_, probe1, probe2, results): + """Plots the frequency response for the two given probes.""" + results.plot_dfft( + [probe1, probe2], frequency_range=Q_((0, 100), "Hz"), yaxis_type="log" + ).show() + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_10.rst b/docs/user_guide/example_10.rst new file mode 100644 index 000000000..f2133d862 --- /dev/null +++ b/docs/user_guide/example_10.rst @@ -0,0 +1,5 @@ +Example 10 +========== + +.. marimo:: example_10.py + :height: 700px diff --git a/docs/user_guide/example_11.py b/docs/user_guide/example_11.py new file mode 100644 index 000000000..8dc1ec73c --- /dev/null +++ b/docs/user_guide/example_11.py @@ -0,0 +1,159 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 11 - Example of rotor connected by a flexible mechanical coupling with parallel, agular or combined misalignment. + ===== + In this example, we use the rotor seen in Example 10 for demonstrating the results obtained when we have a misalignment fault present in the system. + """) + return + + +app._unparsable_cell( + r""" + import numpy as np + + import ross as rs + from ross.faults import * + from ross.units import Q_ + from ross.probe import Probe + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + """, + name="_" +) + + +@app.cell +def _(np, rs): + """Create example rotor with given number of elements.""" + steel2 = rs.Material(name="Steel", rho=7850, E=2.17e11, G_s=81.2e9) + # Rotor with 6 DoFs, with internal damping, with 10 shaft elements, 2 disks and 2 bearings. + i_d = 0 + o_d = 0.019 + n = 33 + + # fmt: off + L = np.array( + [0 , 25, 64, 104, 124, 143, 175, 207, 239, 271, + 303, 335, 345, 355, 380, 408, 436, 466, 496, 526, + 556, 586, 614, 647, 657, 667, 702, 737, 772, 807, + 842, 862, 881, 914] + )/ 1000 + # fmt: on + + L = [L[i] - L[i - 1] for i in range(1, len(L))] + + shaft_elem = [ + rs.ShaftElement( + material=steel2, + L=l, + idl=i_d, + odl=o_d, + idr=i_d, + odr=o_d, + alpha=8.0501, + beta=1.0e-5, + rotary_inertia=True, + shear_effects=True, + ) + for l in L + ] + + Id = 0.003844540885417 + Ip = 0.007513248437500 + + disk0 = rs.DiskElement(n=12, m=2.6375, Id=Id, Ip=Ip) + disk1 = rs.DiskElement(n=24, m=2.6375, Id=Id, Ip=Ip) + + kxx1 = 4.40e5 + kyy1 = 4.6114e5 + kzz = 0 + cxx1 = 27.4 + cyy1 = 2.505 + czz = 0 + kxx2 = 9.50e5 + kyy2 = 1.09e8 + cxx2 = 50.4 + cyy2 = 100.4553 + + bearing0 = rs.BearingElement( + n=4, kxx=kxx1, kyy=kyy1, cxx=cxx1, cyy=cyy1, kzz=kzz, czz=czz + ) + bearing1 = rs.BearingElement( + n=31, kxx=kxx2, kyy=kyy2, cxx=cxx2, cyy=cyy2, kzz=kzz, czz=czz + ) + + rotor = rs.Rotor(shaft_elem, [disk0, disk1], [bearing0, bearing1]) + return (rotor,) + + +@app.cell +def _(Probe, np): + """Inserting a mass and phase unbalance and defining the local response.""" + mass_unb = [5e-4, 0] + phase_unb = [-np.pi / 2, 0] + node_unb = [12, 24] + probe1 = Probe(14, 0) + probe2 = Probe(22, 0) + return mass_unb, node_unb, phase_unb, probe1, probe2 + + +@app.cell +def _(Q_, mass_unb, node_unb, np, phase_unb, rotor): + """Calculate the response of the rotor connected by a flexible mechanical coupling with parallel, + angular or combined misalignment.""" + results = rotor.run_misalignment( + coupling="flex", + mis_type="parallel", # change here the type: mis_type="angular" or mis_type="combined" + radial_stiffness=40e3, + bending_stiffness=38e3, + mis_distance_x=2e-4, + mis_distance_y=2e-4, + mis_angle=5 * np.pi / 180, + input_torque=0, + load_torque=0, + n=0, + speed=Q_(1200, "RPM"), + node=node_unb, + unbalance_magnitude=mass_unb, + unbalance_phase=phase_unb, + t=np.arange(0, 5, 0.0001), + model_reduction={"num_modes": 12}, # Pseudo-modal method, optional + ) + return (results,) + + +@app.cell +def _(probe1, probe2, results): + """Plots the time response for the two given probes.""" + results.plot_1d([probe1, probe2]).show() + return + + +@app.cell +def _(Q_, probe1, probe2, results): + """Plots the frequency response for the two given probes.""" + results.plot_dfft( + [probe1, probe2], frequency_range=Q_((0, 100), "Hz"), yaxis_type="log" + ).show() + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_11.rst b/docs/user_guide/example_11.rst new file mode 100644 index 000000000..255762ab9 --- /dev/null +++ b/docs/user_guide/example_11.rst @@ -0,0 +1,5 @@ +Example 11 +========== + +.. marimo:: example_11.py + :height: 700px diff --git a/docs/user_guide/example_12.py b/docs/user_guide/example_12.py new file mode 100644 index 000000000..f76919f88 --- /dev/null +++ b/docs/user_guide/example_12.py @@ -0,0 +1,154 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 12 - Example of impact phenomena in rotor-casing dynamical systems. + ===== + + In this example, we use the rotor seen in Example 10 for demonstrating the results obtained when we have vibrations due to contact between a rotor and a casing or stator for example. + """) + return + + +app._unparsable_cell( + r""" + import numpy as np + + import ross as rs + from ross.faults import * + from ross.units import Q_ + from ross.probe import Probe + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + """, + name="_" +) + + +@app.cell +def _(np, rs): + """Create example rotor with given number of elements.""" + steel2 = rs.Material(name="Steel", rho=7850, E=2.17e11, Poisson=0.2992610837438423) + # Rotor with 6 DoFs, with internal damping, with 10 shaft elements, 2 disks and 2 bearings. + i_d = 0 + o_d = 0.019 + n = 33 + + # fmt: off + L = np.array( + [0 , 25, 64, 104, 124, 143, 175, 207, 239, 271, + 303, 335, 345, 355, 380, 408, 436, 466, 496, 526, + 556, 586, 614, 647, 657, 667, 702, 737, 772, 807, + 842, 862, 881, 914] + )/ 1000 + # fmt: on + + L = [L[i] - L[i - 1] for i in range(1, len(L))] + + shaft_elem = [ + rs.ShaftElement( + material=steel2, + L=l, + idl=i_d, + odl=o_d, + idr=i_d, + odr=o_d, + alpha=8.0501, + beta=1.0e-5, + rotary_inertia=True, + shear_effects=True, + ) + for l in L + ] + + Id = 0.003844540885417 + Ip = 0.007513248437500 + + disk0 = rs.DiskElement(n=12, m=2.6375, Id=Id, Ip=Ip) + disk1 = rs.DiskElement(n=24, m=2.6375, Id=Id, Ip=Ip) + + kxx1 = 4.40e5 + kyy1 = 4.6114e5 + kzz = 0 + cxx1 = 27.4 + cyy1 = 2.505 + czz = 0 + kxx2 = 9.50e5 + kyy2 = 1.09e8 + cxx2 = 50.4 + cyy2 = 100.4553 + + bearing0 = rs.BearingElement( + n=4, kxx=kxx1, kyy=kyy1, cxx=cxx1, cyy=cyy1, kzz=kzz, czz=czz + ) + bearing1 = rs.BearingElement( + n=31, kxx=kxx2, kyy=kyy2, cxx=cxx2, cyy=cyy2, kzz=kzz, czz=czz + ) + + rotor = rs.Rotor(shaft_elem, [disk0, disk1], [bearing0, bearing1]) + return (rotor,) + + +@app.cell +def _(Probe, np): + """Inserting a mass and phase unbalance and defining the local response.""" + mass_unb = [2e-4, 0] + phase_unb = [-np.pi / 2, 0] + node_unb = [12, 24] + probe1 = Probe(14, 0) + probe2 = Probe(22, 0) + return mass_unb, node_unb, phase_unb, probe1, probe2 + + +@app.cell +def _(Q_, mass_unb, node_unb, np, phase_unb, rotor): + """Calculate the response when a rotor makes contact with a casing or stator.""" + results = rotor.run_rubbing( + distance=7.95e-5, + contact_stiffness=1.1e6, + contact_damping=40, + friction_coeff=0.3, + n=12, + speed=Q_(1200, "RPM"), + node=node_unb, + unbalance_magnitude=mass_unb, + unbalance_phase=phase_unb, + t=np.arange(0, 5, 0.0001), + model_reduction={"num_modes": 12}, # Pseudo-modal method, optional + ) + return (results,) + + +@app.cell +def _(probe1, probe2, results): + """Plots the time response for the two given probes.""" + results.plot_1d([probe1, probe2]).show() + return + + +@app.cell +def _(Q_, probe1, probe2, results): + """Plots the frequency response for the two given probes.""" + results.plot_dfft( + [probe1, probe2], frequency_range=Q_((0, 100), "Hz"), yaxis_type="log" + ).show() + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_12.rst b/docs/user_guide/example_12.rst new file mode 100644 index 000000000..fd953aeb4 --- /dev/null +++ b/docs/user_guide/example_12.rst @@ -0,0 +1,5 @@ +Example 12 +========== + +.. marimo:: example_12.py + :height: 700px diff --git a/docs/user_guide/example_13.py b/docs/user_guide/example_13.py new file mode 100644 index 000000000..714dc5cc2 --- /dev/null +++ b/docs/user_guide/example_13.py @@ -0,0 +1,170 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 13 - Uncertainties on bearings coefficients + ========================================== + In this example, we use the rotor seen in Example 5.9.4 from {cite}`friswell2010dynamics`. + + This system is the same as that of Example 3, except that now, we'll considerer some level of uncertainties on bearing direct coefficients (`kxx`, `kyy`, `cxx`, `cyy`). + """) + return + + +@app.cell +def _(): + import ross as rs + import ross.stochastic as srs + import numpy as np + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return np, rs, srs + + +@app.cell +def _(rs): + # Deterministic Shaft Elements + Steel = rs.steel + shaft = [rs.ShaftElement(L=0.25, material=Steel, idl=0, odl=0.05) for i in range(6)] + + # Deterministic Disk Elements + disk1 = rs.DiskElement.from_geometry( + n=2, + material=Steel, + width=0.07, + i_d=0.05, + o_d=0.28, + ) + + disk2 = rs.DiskElement.from_geometry( + n=4, + material=Steel, + width=0.07, + i_d=0.05, + o_d=0.35, + ) + disks = [disk1, disk2] + return disks, shaft + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + In this example, let's consider an uniform distribuition for the coefficients. Because it's a demonstrative example, we'll not use too many samples, to avoid taking too long to run the simulation. + + We can use `numpy.random` package to generate random values for our variables. + + #### Stochastic Bearing Elements + """) + return + + +@app.cell +def _(np, srs): + # random variables must have the same size + kxx = np.random.uniform(low=1e6, high=3e6, size=50) + cxx = np.random.uniform(low=1e2, high=2e2, size=50) + + bearing1 = srs.ST_BearingElement(n=0, kxx=kxx, cxx=cxx, is_random=["kxx", "cxx"]) + bearing2 = srs.ST_BearingElement(n=6, kxx=kxx, cxx=cxx, is_random=["kxx", "cxx"]) + bearings = [bearing1, bearing2] + return (bearings,) + + +@app.cell +def _(bearings, disks, shaft, srs): + # Building random instances for a rotor model + + rand_rotor = srs.ST_Rotor( + shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings + ) + + # Number of samples + print("Number of samples:", len(list(iter(rand_rotor)))) + return (rand_rotor,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Plotting a random sample + + We can use `numpy.random.choice` to take a random rotor object. Then, we can use the same functions than to `Rotor` class. + """) + return + + +@app.cell +def _(np, rand_rotor): + sample = np.random.choice(list(iter(rand_rotor))) + sample.plot_rotor() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Running Stochastic Campbell Diagram + """) + return + + +@app.cell +def _(np, rand_rotor): + speed_range = np.linspace(0, 600, 31) + camp = rand_rotor.run_campbell(speed_range) + return (camp,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Plotting Stochastic Campbell Diagram + """) + return + + +@app.cell +def _(camp): + # choose the desirable percentiles or confidence intervals + camp.plot_nat_freq(conf_interval=[95], width=950, height=700) + return + + +@app.cell +def _(camp): + # choose the desirable percentiles or confidence intervals + camp.plot_log_dec(conf_interval=[95], width=950, height=700) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_13.rst b/docs/user_guide/example_13.rst new file mode 100644 index 000000000..aede037bc --- /dev/null +++ b/docs/user_guide/example_13.rst @@ -0,0 +1,5 @@ +Example 13 +========== + +.. marimo:: example_13.py + :height: 700px diff --git a/docs/user_guide/example_14.py b/docs/user_guide/example_14.py new file mode 100644 index 000000000..9d99f908a --- /dev/null +++ b/docs/user_guide/example_14.py @@ -0,0 +1,190 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 14 - Uncertainties on material properties + ======================================== + In this example, we use the rotor seen in Example 5.9.4 from {cite}`friswell2010dynamics`. + + This system is the same as that of Example 3, but now we'll work with uncertainties on material properties, representing a fault in the shaft. We will apply the uncertainties at the central elements. + """) + return + + +@app.cell +def _(): + import ross as rs + import ross.stochastic as srs + import numpy as np + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return np, rs, srs + + +@app.cell +def _(np, rs, srs): + # Deterministic Shaft Elements + Steel = rs.steel + shaft = [rs.ShaftElement(L=0.25, material=Steel, idl=0, odl=0.05) for i in range(4)] + + # Material with random properties + size = 50 + E = np.random.uniform(low=190e9, high=210e9, size=size) + rho = np.random.uniform(low=7000, high=7810, size=size) + G_s = np.random.uniform(low=70.5e9, high=81.2e9, size=size) + rand_mat = srs.ST_Material(name="Steel", rho=rho, E=E, G_s=G_s) + + # Stochastic Shaft Elements + rand_el = srs.ST_ShaftElement( + L=0.25, + idl=0, + odl=0.05, + material=rand_mat, + is_random=["material"], + ) + + # Inserting stochastic elements to the shaft elements list + shaft.insert(2, rand_el) + shaft.insert(3, rand_el) + + # Deterministic Disk Elements + disk1 = rs.DiskElement.from_geometry( + n=2, + material=Steel, + width=0.07, + i_d=0.05, + o_d=0.28, + ) + + disk2 = rs.DiskElement.from_geometry( + n=4, + material=Steel, + width=0.07, + i_d=0.05, + o_d=0.35, + ) + disks = [disk1, disk2] + return disks, shaft + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + In this example, let's consider an uniform distribuition for the coefficients. Because it's a demonstrative example, we'll not use too many samples, to avoid taking too long to run the simulation. + + We can use `numpy.random` package to generate random values for our variables. + + #### Stochastic Bearing Elements + """) + return + + +@app.cell +def _(rs): + # random variables must have the same size + bearing1 = rs.BearingElement(n=0, kxx=1e6, cxx=2e2) + bearing2 = rs.BearingElement(n=6, kxx=1e6, cxx=2e2) + bearings = [bearing1, bearing2] + return (bearings,) + + +@app.cell +def _(bearings, disks, shaft, srs): + # Building random instances for a rotor model + + rand_rotor = srs.ST_Rotor( + shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings + ) + + # Number of samples + print("Number of samples:", len(list(iter(rand_rotor)))) + return (rand_rotor,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Plotting a random sample + + We can use `numpy.random.choice` to take a random rotor object. Then, we can use the same functions than to `Rotor` class. + """) + return + + +@app.cell +def _(np, rand_rotor): + sample = np.random.choice(list(iter(rand_rotor))) + fig = sample.plot_rotor() + fig.show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Running Stochastic Campbell Diagram + """) + return + + +@app.cell +def _(np, rand_rotor): + speed_range = np.linspace(0, 600, 31) + camp = rand_rotor.run_campbell(speed_range) + return (camp,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Plotting Stochastic Campbell Diagram + """) + return + + +@app.cell +def _(camp): + # choose the desirable percentiles or confidence intervals + fig1 = camp.plot_nat_freq(conf_interval=[95], width=950, height=700) + fig1.show() + return + + +@app.cell +def _(camp): + # choose the desirable percentiles or confidence intervals + fig2 = camp.plot_log_dec(conf_interval=[95], width=950, height=700) + fig2.show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_14.rst b/docs/user_guide/example_14.rst new file mode 100644 index 000000000..b71bf345c --- /dev/null +++ b/docs/user_guide/example_14.rst @@ -0,0 +1,5 @@ +Example 14 +========== + +.. marimo:: example_14.py + :height: 700px diff --git a/docs/user_guide/example_15.py b/docs/user_guide/example_15.py new file mode 100644 index 000000000..1f2b444be --- /dev/null +++ b/docs/user_guide/example_15.py @@ -0,0 +1,162 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 15 - Example to evaluate the influence of transverse cracks in a rotating shaft. + ===== + In this example, we use the rotor seen in Example 10 for demonstrating the results obtained when we have a transversal crack present in the rotor system. In this case, the breathing behavior of the considered transversal crack is described according to the model proposed by {cite}`mayes1984analysis` and {cite}`gasch1993survey`. + """) + return + + +app._unparsable_cell( + r""" + import numpy as np + + import ross as rs + from ross.faults import * + from ross.units import Q_ + from ross.probe import Probe + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + """, + name="_" +) + + +@app.cell +def _(np, rs): + """Create example rotor with given number of elements.""" + steel2 = rs.Material(name="Steel", rho=7850, E=2.17e11, Poisson=0.2992610837438423) + # Rotor with 6 DoFs, with internal damping, with 10 shaft elements, 2 disks and 2 bearings. + i_d = 0 + o_d = 0.019 + n = 33 + + # fmt: off + L = np.array( + [0 , 25, 64, 104, 124, 143, 175, 207, 239, 271, + 303, 335, 345, 355, 380, 408, 436, 466, 496, 526, + 556, 586, 614, 647, 657, 667, 702, 737, 772, 807, + 842, 862, 881, 914] + )/ 1000 + # fmt: on + + L = [L[i] - L[i - 1] for i in range(1, len(L))] + + shaft_elem = [ + rs.ShaftElement( + material=steel2, + L=l, + idl=i_d, + odl=o_d, + idr=i_d, + odr=o_d, + alpha=8.0501, + beta=1.0e-5, + rotary_inertia=True, + shear_effects=True, + ) + for l in L + ] + + Id = 0.003844540885417 + Ip = 0.007513248437500 + + disk0 = rs.DiskElement(n=12, m=2.6375, Id=Id, Ip=Ip) + disk1 = rs.DiskElement(n=24, m=2.6375, Id=Id, Ip=Ip) + + kxx1 = 4.40e5 + kyy1 = 4.6114e5 + kzz = 0 + cxx1 = 27.4 + cyy1 = 2.505 + czz = 0 + kxx2 = 9.50e5 + kyy2 = 1.09e8 + cxx2 = 50.4 + cyy2 = 100.4553 + + bearing0 = rs.BearingElement( + n=4, kxx=kxx1, kyy=kyy1, cxx=cxx1, cyy=cyy1, kzz=kzz, czz=czz + ) + bearing1 = rs.BearingElement( + n=31, kxx=kxx2, kyy=kyy2, cxx=cxx2, cyy=cyy2, kzz=kzz, czz=czz + ) + + rotor = rs.Rotor(shaft_elem, [disk0, disk1], [bearing0, bearing1]) + return (rotor,) + + +@app.cell +def _(Probe, np): + """Inserting a mass and phase unbalance and defining the local response.""" + mass_unb = [5e-4, 0] + phase_unb = [-np.pi / 2, 0] + node_unb = [12, 24] + probe1 = Probe(14, 0) + probe2 = Probe(22, 0) + return mass_unb, node_unb, phase_unb, probe1, probe2 + + +@app.cell +def _(Q_, mass_unb, node_unb, np, phase_unb, rotor): + results = rotor.run_crack( + depth_ratio=0.2, + n=18, + speed=Q_(1200, "RPM"), + crack_model="Gasch", # or crack_model="Mayes" + node=node_unb, + unbalance_magnitude=mass_unb, + unbalance_phase=phase_unb, + t=np.arange(0, 5, 0.0001), + model_reduction={"num_modes": 12}, # Pseudo-modal method, optional + ) + return (results,) + + +@app.cell +def _(probe1, probe2, results): + """Plots the time response for the two given probes.""" + results.plot_1d([probe1, probe2]).show() + return + + +@app.cell +def _(Q_, probe1, probe2, results): + """Plots the frequency response for the two given probes.""" + results.plot_dfft( + [probe1, probe2], frequency_range=Q_((0, 100), "Hz"), yaxis_type="log" + ).show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_15.rst b/docs/user_guide/example_15.rst new file mode 100644 index 000000000..9d0848b5d --- /dev/null +++ b/docs/user_guide/example_15.rst @@ -0,0 +1,5 @@ +Example 15 +========== + +.. marimo:: example_15.py + :height: 700px diff --git a/docs/user_guide/example_16.py b/docs/user_guide/example_16.py new file mode 100644 index 000000000..17001c9ea --- /dev/null +++ b/docs/user_guide/example_16.py @@ -0,0 +1,124 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 16 - Look at different models of the disk-shaft interface + + In this example, we use the rotor seen in Example 5.8.3 from {cite}`friswell2010dynamics` + and we make the same comparison looking at 4 different ways of modelling the shaft-disk interface: + + ``` + Consider a hollow shaft carrying a central disk and mounted on + short, rigid bearings at each end. The shaft is 1.2 m long and it has an outside + diameter of 80 mm and an inside diameter of 30 mm. The central disk has a diameter + of 400 mm and a thickness of 80 mm. The shaft and disk have a modulus + of elasticity of 200GN/m2 and a density of 7,800kg/m3. Poisson’s ratio is 0.27. + This is the same as the rotor used in Example 5.8.1. The rotor, consisting of + the shaft and central disk, spins at 3,000 rev/min. Investigate the effect of the + shaft-disk interface models on the natural frequencies of the machine. + ``` + """) + return + + +@app.cell +def _(): + import ross as rs + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return (rs,) + + +@app.cell +def _(rs): + Q_ = rs.Q_ + steel = rs.Material("steel", rho=7800, E=200e9, Poisson=0.27) + return Q_, steel + + +@app.cell +def _(): + L = 0.2 + idl = 0.03 + odl = 0.08 + return L, idl, odl + + +@app.cell +def _(L, idl, odl, rs, steel): + N = 6 + _shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for i in range(N)] + _bearings = [rs.BearingElement(n=0, kxx=1e+20, cxx=0), rs.BearingElement(n=len(_shaft), kxx=1e+20, cxx=0)] + _disks = [rs.DiskElement.from_geometry(n=3, material=steel, width=0.08, o_d=0.4, i_d=0.08)] + rotor1 = rs.Rotor(shaft_elements=_shaft, disk_elements=_disks, bearing_elements=_bearings) + rotor1.plot_rotor() + return (rotor1,) + + +@app.cell +def _(L, idl, odl, rs, steel): + _shaft = [rs.ShaftElement(n=0, L=L, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=1, L=L, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=2, L=0.16, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=3, L=0.04, idl=idl, odl=0.16, material=steel), rs.ShaftElement(n=4, L=0.04, idl=idl, odl=0.16, material=steel), rs.ShaftElement(n=5, L=0.16, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=6, L=L, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=7, L=L, idl=idl, odl=odl, material=steel)] + _bearings = [rs.BearingElement(n=0, kxx=1e+20, cxx=0), rs.BearingElement(n=len(_shaft), kxx=1e+20, cxx=0)] + _disks = [rs.DiskElement.from_geometry(n=4, material=steel, width=0.08, o_d=0.4, i_d=0.16)] + rotor2 = rs.Rotor(shaft_elements=_shaft, disk_elements=_disks, bearing_elements=_bearings) + rotor2.plot_rotor() + return (rotor2,) + + +@app.cell +def _(L, idl, odl, rs, steel): + _shaft = [rs.ShaftElement(n=0, L=L, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=1, L=L, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=2, L=0.16, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=3, L=0.04, idl=idl, odl=0.16, material=steel), rs.ShaftElement(n=4, L=0.04, idl=idl, odl=0.16, material=steel), rs.ShaftElement(n=5, L=0.16, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=6, L=L, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=7, L=L, idl=idl, odl=odl, material=steel)] + _bearings = [rs.BearingElement(n=0, kxx=1e+20, cxx=0), rs.BearingElement(n=len(_shaft), kxx=1e+20, cxx=0)] + _disks = [rs.DiskElement.from_geometry(n=3, material=steel, width=0.02, o_d=0.4, i_d=0.16), rs.DiskElement.from_geometry(n=4, material=steel, width=0.04, o_d=0.4, i_d=0.16), rs.DiskElement.from_geometry(n=5, material=steel, width=0.02, o_d=0.4, i_d=0.16)] + rotor3 = rs.Rotor(shaft_elements=_shaft, disk_elements=_disks, bearing_elements=_bearings) + rotor3.plot_rotor() + return (rotor3,) + + +@app.cell +def _(L, idl, odl, rs, steel): + _shaft = [rs.ShaftElement(n=0, L=L, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=1, L=L, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=2, L=0.16, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=3, L=0.08, idl=idl, odl=0.4, material=steel), rs.ShaftElement(n=4, L=0.16, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=5, L=L, idl=idl, odl=odl, material=steel), rs.ShaftElement(n=6, L=L, idl=idl, odl=odl, material=steel)] + _bearings = [rs.BearingElement(n=0, kxx=1e+20, cxx=0), rs.BearingElement(n=len(_shaft), kxx=1e+20, cxx=0)] + rotor4 = rs.Rotor(shaft_elements=_shaft, bearing_elements=_bearings) + rotor4.plot_rotor() + return (rotor4,) + + +@app.cell +def _(Q_, rotor1, rotor2, rotor3, rotor4, rs): + for i, rotor in enumerate([rotor1, rotor2, rotor3, rotor4]): + modal = rotor.run_modal(rs.Q_(3000, "RPM")) + print(f"Rotor {i + 1}: {Q_(modal.wd, 'rad/s').to('Hz'):.4f}") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_16.rst b/docs/user_guide/example_16.rst new file mode 100644 index 000000000..e9fdf6e9c --- /dev/null +++ b/docs/user_guide/example_16.rst @@ -0,0 +1,5 @@ +Example 16 +========== + +.. marimo:: example_16.py + :height: 700px diff --git a/docs/user_guide/example_17.py b/docs/user_guide/example_17.py new file mode 100644 index 000000000..da8fbc38f --- /dev/null +++ b/docs/user_guide/example_17.py @@ -0,0 +1,137 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 17 - Features of Eigenvalues and Eigenvectors - Isotropic Bearings + + This example is based on Example 5.9.1 from {cite}`friswell2010dynamics`. + + + Isotropic Bearings. A 1.5-m-long shaft, shown in Figure 5.27, + has a diameter of 0.05 m. The disks are keyed to the shaft at 0.5 and 1 m from + one end. The left disk is 0.07 m thick with a diameter of 0.28 m; the right disk + is 0.07 m thick with a diameter of 0.35 m. For the shaft, E = 211 GN/m2 and + G = 81.2 GN/m2. There is no internal shaft damping. For both the shaft and the + disks, p = 7,810 kg/m3. The shaft is supported by identical bearings at its ends.Constant + + These bearings are isotropic and have a stiffness of 1 MN/m in both the x and + y directions. The bearings contribute no additional stiffness to the rotational + degrees of freedom and there is no damping or cross-coupling in the bearings. + Create an FE model of the shaft using six Timoshenko beam elements and + investigate the dynamics of the machine at 0 and 4,000 rev/min. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + from IPython.display import display + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return display, rs + + +@app.cell +def _(rs): + Q_ = rs.Q_ + return (Q_,) + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.2e9, rho=7810) + return (steel,) + + +@app.cell +def _(rs, steel): + L = 0.25 + N = 6 + idl = 0 + odl = 0.05 + + shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for i in range(N)] + bearings = [ + rs.BearingElement(n=0, kxx=1e6, cxx=0, scale_factor=2), + rs.BearingElement(n=len(shaft), kxx=1e6, cxx=0, scale_factor=2), + ] + disks = [ + rs.DiskElement.from_geometry( + n=2, material=steel, width=0.07, i_d=odl, o_d=0.28, scale_factor="mass" + ), + rs.DiskElement.from_geometry( + n=4, material=steel, width=0.07, i_d=odl, o_d=0.35, scale_factor="mass" + ), + ] + + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor.plot_rotor() + return (rotor,) + + +@app.cell +def _(Q_, rotor): + campbell = rotor.run_campbell( + speed_range=Q_(list(range(0, 4500, 50)), "RPM"), frequencies=7 + ) + return (campbell,) + + +@app.cell +def _(campbell): + campbell.plot(frequency_units="RPM") + return + + +@app.cell +def _(Q_, rotor): + modal = rotor.run_modal(speed=Q_(4000, "RPM"), num_modes=14) + return (modal,) + + +@app.cell +def _(display, modal): + for _mode in range(7): + display(modal.plot_mode_3d(_mode, frequency_units='Hz')) + return + + +@app.cell +def _(display, modal): + for _mode in range(7): + display(modal.plot_orbit(_mode, nodes=[2, 4])) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_17.rst b/docs/user_guide/example_17.rst new file mode 100644 index 000000000..3cc434226 --- /dev/null +++ b/docs/user_guide/example_17.rst @@ -0,0 +1,5 @@ +Example 17 +========== + +.. marimo:: example_17.py + :height: 700px diff --git a/docs/user_guide/example_18.py b/docs/user_guide/example_18.py new file mode 100644 index 000000000..5a2938368 --- /dev/null +++ b/docs/user_guide/example_18.py @@ -0,0 +1,129 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 18 - Features of Eigenvalues and Eigenvectors - Anisotropic Bearings + + This example is based on Example 5.9.2 from {cite}`friswell2010dynamics`. + + + Anisotropic Bearings. This system is the same as that of Example 5.9.1 except that the isotropic bearings are replaced by anisotropic bearings. + Both bearings have a stiffness of 1 MN/m in the x direction and 0.8 MN/m in the + y direction. Calculate the eigenvalues and mode shapes at 0 and 4,000 rev/min + and plot the natural frequency map for rotational speeds up to 4,500 rev/min.​ + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + from IPython.display import display + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return display, rs + + +@app.cell +def _(rs): + Q_ = rs.Q_ + return (Q_,) + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.2e9, rho=7810) + return (steel,) + + +@app.cell +def _(rs, steel): + L = 0.25 + N = 6 + idl = 0 + odl = 0.05 + + shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for i in range(N)] + bearings = [ + rs.BearingElement(n=0, kxx=1e6, kyy=0.8e6, cxx=0, scale_factor=2), + rs.BearingElement(n=len(shaft), kxx=1e6, kyy=0.8e6, cxx=0, scale_factor=2), + ] + disks = [ + rs.DiskElement.from_geometry( + n=2, material=steel, width=0.07, i_d=odl, o_d=0.28, scale_factor="mass" + ), + rs.DiskElement.from_geometry( + n=4, material=steel, width=0.07, i_d=odl, o_d=0.35, scale_factor="mass" + ), + ] + + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor.plot_rotor() + return (rotor,) + + +@app.cell +def _(Q_, rotor): + campbell = rotor.run_campbell( + speed_range=Q_(list(range(0, 4500, 50)), "RPM"), frequencies=7 + ) + return (campbell,) + + +@app.cell +def _(campbell): + campbell.plot(frequency_units="RPM") + return + + +@app.cell +def _(Q_, rotor): + modal = rotor.run_modal(speed=Q_(4000, "RPM"), num_modes=14) + return (modal,) + + +@app.cell +def _(display, modal): + for _mode in range(7): + display(modal.plot_mode_3d(_mode, frequency_units='Hz')) + return + + +@app.cell +def _(display, modal): + for _mode in range(7): + display(modal.plot_orbit(_mode, nodes=[2, 4])) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_18.rst b/docs/user_guide/example_18.rst new file mode 100644 index 000000000..c179020f9 --- /dev/null +++ b/docs/user_guide/example_18.rst @@ -0,0 +1,5 @@ +Example 18 +========== + +.. marimo:: example_18.py + :height: 700px diff --git a/docs/user_guide/example_19.py b/docs/user_guide/example_19.py new file mode 100644 index 000000000..e5aea9847 --- /dev/null +++ b/docs/user_guide/example_19.py @@ -0,0 +1,129 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 19 - Features of Eigenvalues and Eigenvectors - System with Mixed Modes + + This example is based on Example 5.9.3 from {cite}`friswell2010dynamics`. + + + System with Mixed Modes. Analyze the system o f Example 5.9.2 + with bearing stiffnesses of l MN/ m in the x direction and 0.2 MN/m in the y + direction. Calculate the eigenvalues and mode shapes at 4,000 rev/min and show + that some modes contain both forward and backward whirling components. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + from IPython.display import display + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return display, rs + + +@app.cell +def _(rs): + Q_ = rs.Q_ + return (Q_,) + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.2e9, rho=7810) + return (steel,) + + +@app.cell +def _(rs, steel): + L = 0.25 + N = 6 + idl = 0 + odl = 0.05 + + shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for i in range(N)] + bearings = [ + rs.BearingElement(n=0, kxx=1e6, kyy=0.2e6, cxx=0, scale_factor=2), + rs.BearingElement(n=len(shaft), kxx=1e6, kyy=0.2e6, cxx=0, scale_factor=2), + ] + disks = [ + rs.DiskElement.from_geometry( + n=2, material=steel, width=0.07, i_d=odl, o_d=0.28, scale_factor="mass" + ), + rs.DiskElement.from_geometry( + n=4, material=steel, width=0.07, i_d=odl, o_d=0.35, scale_factor="mass" + ), + ] + + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor.plot_rotor() + return (rotor,) + + +@app.cell +def _(Q_, rotor): + campbell = rotor.run_campbell( + speed_range=Q_(list(range(0, 4500, 50)), "RPM"), frequencies=7 + ) + return (campbell,) + + +@app.cell +def _(campbell): + campbell.plot(frequency_units="RPM") + return + + +@app.cell +def _(Q_, rotor): + modal = rotor.run_modal(speed=Q_(4000, "RPM"), num_modes=14) + return (modal,) + + +@app.cell +def _(display, modal): + for _mode in range(7): + display(modal.plot_mode_3d(_mode, frequency_units='Hz')) + return + + +@app.cell +def _(display, modal): + for _mode in range(7): + display(modal.plot_orbit(_mode, nodes=[2, 4])) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_19.rst b/docs/user_guide/example_19.rst new file mode 100644 index 000000000..67366516b --- /dev/null +++ b/docs/user_guide/example_19.rst @@ -0,0 +1,5 @@ +Example 19 +========== + +.. marimo:: example_19.py + :height: 700px diff --git a/docs/user_guide/example_2.py b/docs/user_guide/example_2.py new file mode 100644 index 000000000..772d7b323 --- /dev/null +++ b/docs/user_guide/example_2.py @@ -0,0 +1,140 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell +async def _(): + """No navegador (Pyodide), instala plotly via micropip antes dos imports.""" + import sys + if "pyodide" in sys.modules: + import micropip + await micropip.install("plotly") + _packages_ready = True + return (_packages_ready,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 2 - Effect of varying slenderness ratio + ========= + In this example, we use the rotor seen in Example 5.8.2 from {cite}`friswell2010dynamics`. + """) + return + + +@app.cell +def _(_packages_ready): + try: + import numpy as np + import plotly.graph_objects as go + import ross as rs + import plotly.io as pio + pio.renderers.default = "notebook" + _deps_ok = True + except ModuleNotFoundError: + _deps_ok = False + np = go = rs = None + return (go, np, rs, _deps_ok) + + +@app.cell +def _(mo, rs, _deps_ok): + if not _deps_ok or rs is None: + mo.md( + r""" + **Notebook no navegador (Pyodide):** A biblioteca **ROSS** não está disponível no browser. + Para rodar com ROSS localmente (use **uv**): `uv run marimo edit docs/user_guide/example_2.py` + """ + ) + steel = None + else: + steel = rs.materials.steel + return (steel,) + + +@app.cell +def _(): + number_of_elements = [2, 3, 4, 5, 6, 7, 8] + return (number_of_elements,) + + +@app.cell +def _(rs, steel, _deps_ok): + if not _deps_ok or rs is None or steel is None: + create_rotor = None + else: + def create_rotor(n_el, R, shear_effects=False, rotary_inertia=False): + """Create example rotor with given number of elements and R ration.""" + L_total = 1 + D = R * L_total + shaft = [rs.ShaftElement(1.0 / n_el, idl=0, odl=D, material=steel, shear_effects=shear_effects, rotary_inertia=rotary_inertia) for _i in range(n_el)] + bearings = [rs.BearingElement(0, kxx=1000000000000000.0, cxx=0), rs.BearingElement(n_el, kxx=1000000000000000.0, cxx=0)] + return rs.Rotor(shaft_elements=shaft, bearing_elements=bearings) + return (create_rotor,) + + +@app.cell +def _(create_rotor, go, np, number_of_elements): + if create_rotor is None or go is None or np is None: + return + _n_eigen = 8 + _R = 0.04 + rotor_80 = create_rotor(80, _R) + modal_80 = rotor_80.run_modal(speed=0, num_modes=2 * _n_eigen) + _errors = np.zeros([len(number_of_elements), _n_eigen]) + for (_i, _n_el) in enumerate(number_of_elements): + _rotor = create_rotor(_n_el, _R) + _modal = _rotor.run_modal(speed=0, num_modes=2 * _n_eigen) + _errors[_i, :] = abs(100 * (_modal.wn[:_n_eigen] - modal_80.wn[:_n_eigen]) / modal_80.wn[:_n_eigen]) + _fig = go.Figure() + for _i in range(_n_eigen): + _fig.add_trace(go.Scatter(x=number_of_elements, y=_errors[:, _i], name=f'Mode {_i}')) + _fig.update_layout(xaxis=dict(title='Number of elements'), yaxis=dict(title='Natural Frequency error(%)', type='log')) + return + + +@app.cell +def _(create_rotor, go, np): + if create_rotor is None or go is None or np is None: + return + _n_el = 6 + R_list = np.linspace(0.0001, 0.15, 10) + _n_eigen = 6 + _errors = np.zeros([len(R_list), _n_eigen]) + for (_i, _R) in enumerate(R_list): + rotor_ref = create_rotor(100, _R, shear_effects=True, rotary_inertia=True) + modal_ref = rotor_ref.run_modal(speed=0) + _rotor = create_rotor(_n_el, _R) + _modal = _rotor.run_modal(speed=0, num_modes=2 * _n_eigen) + _errors[_i, :] = abs(100 * (_modal.wn[:_n_eigen] - modal_ref.wn[:_n_eigen]) / modal_ref.wn[:_n_eigen]) + _fig = go.Figure() + for _i in range(_n_eigen): + _fig.add_trace(go.Scatter(x=R_list, y=_errors[:, _i], name=f'Mode {_i}')) + _fig.update_layout(xaxis=dict(title='Slenderness ratio'), yaxis=dict(title='Natural Frequency error(%)', type='log')) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_2.rst b/docs/user_guide/example_2.rst new file mode 100644 index 000000000..73a9f7dfe --- /dev/null +++ b/docs/user_guide/example_2.rst @@ -0,0 +1,5 @@ +Example 2 +========= + +.. marimo:: example_2.py + :height: 700px diff --git a/docs/user_guide/example_20.py b/docs/user_guide/example_20.py new file mode 100644 index 000000000..04c282386 --- /dev/null +++ b/docs/user_guide/example_20.py @@ -0,0 +1,132 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 20 - Features of Eigenvalues and Eigenvectors - Cross-Coupling in the Bearings + + This example is based on Example 5.9.4 from {cite}`friswell2010dynamics`. + + Cross-Coupling in the Bearings. This system is the same as that of + Example 5.9.1 except that some coupling is introduced in the bearings between + the x and y directions. T he bearings have direct stiffnesses of 1 MN/m and cross + coupling stiffnesses of 0.5 MN/m. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + from IPython.display import display + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return display, rs + + +@app.cell +def _(rs): + Q_ = rs.Q_ + return (Q_,) + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.2e9, rho=7810) + return (steel,) + + +@app.cell +def _(rs, steel): + L = 0.25 + N = 6 + idl = 0 + odl = 0.05 + + shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for i in range(N)] + bearings = [ + rs.BearingElement( + n=0, kxx=1e6, kyy=1e6, kxy=0.5e6, kyx=0.5e6, cxx=0, scale_factor=2 + ), + rs.BearingElement( + n=len(shaft), kxx=1e6, kyy=1e6, kxy=0.5e6, kyx=0.5e6, cxx=0, scale_factor=2 + ), + ] + disks = [ + rs.DiskElement.from_geometry( + n=2, material=steel, width=0.07, i_d=odl, o_d=0.28, scale_factor="mass" + ), + rs.DiskElement.from_geometry( + n=4, material=steel, width=0.07, i_d=odl, o_d=0.35, scale_factor="mass" + ), + ] + + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor.plot_rotor() + return (rotor,) + + +@app.cell +def _(Q_, rotor): + campbell = rotor.run_campbell( + speed_range=Q_(list(range(0, 4500, 50)), "RPM"), frequencies=7 + ) + return (campbell,) + + +@app.cell +def _(campbell): + campbell.plot(frequency_units="RPM") + return + + +@app.cell +def _(Q_, rotor): + modal = rotor.run_modal(speed=Q_(4000, "RPM"), num_modes=14) + return (modal,) + + +@app.cell +def _(display, modal): + for _mode in range(7): + display(modal.plot_mode_3d(_mode, frequency_units='Hz')) + return + + +@app.cell +def _(display, modal): + for _mode in range(7): + display(modal.plot_orbit(_mode, nodes=[2, 4])) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_20.rst b/docs/user_guide/example_20.rst new file mode 100644 index 000000000..1ed37141d --- /dev/null +++ b/docs/user_guide/example_20.rst @@ -0,0 +1,5 @@ +Example 20 +========== + +.. marimo:: example_20.py + :height: 700px diff --git a/docs/user_guide/example_21.py b/docs/user_guide/example_21.py new file mode 100644 index 000000000..07093703f --- /dev/null +++ b/docs/user_guide/example_21.py @@ -0,0 +1,147 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 21 - Features of Eigenvalues and Eigenvectors - Hydrodynamic Bearings + + This example is based on Example 5.9.6 from {cite}`friswell2010dynamics`. + Example 7 from the documentation uses the `BearingFluidFlow` class to create a hydrodynamic bearing. + + Here we are going to use the `CylindricalBearing` class, which is implemented based on {cite}`friswell2010dynamics`. + + Hydrodynamic Bearings. Repeat the analysis of Example 5.9.1 + when the bearings are replaced with hydrodynamic bearings. The oil-film bear + ings have a diameter of 100 mm, are 30 mm long, and each supports a static load + of 525 N, which represents half of the weight of the rotor. The radial clearance in + the bearings is 0.1 mm and the oil film has a viscosity of 0.1 Pa s. These bearings + have the same characteristics as Example 5.5.1. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + from IPython.display import display + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return display, rs + + +@app.cell +def _(rs): + Q_ = rs.Q_ + return (Q_,) + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.2e9, rho=7810) + return (steel,) + + +@app.cell +def _(Q_, rs, steel): + L = 0.25 + N = 6 + idl = 0 + odl = 0.05 + + shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for i in range(N)] + bearings = [ + rs.CylindricalBearing( + n=0, + speed=Q_(list(range(0, 5000, 50)), "RPM"), + weight=525, + bearing_length=Q_(30, "mm"), + journal_diameter=Q_(100, "mm"), + radial_clearance=Q_(0.1, "mm"), + oil_viscosity=0.1, + ), + rs.CylindricalBearing( + n=len(shaft), + speed=Q_(list(range(0, 5000, 50)), "RPM"), + weight=525, + bearing_length=Q_(30, "mm"), + journal_diameter=Q_(100, "mm"), + radial_clearance=Q_(0.1, "mm"), + oil_viscosity=0.1, + ), + ] + disks = [ + rs.DiskElement.from_geometry( + n=2, material=steel, width=0.07, i_d=odl, o_d=0.28, scale_factor="mass" + ), + rs.DiskElement.from_geometry( + n=4, material=steel, width=0.07, i_d=odl, o_d=0.35, scale_factor="mass" + ), + ] + + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor.plot_rotor() + return (rotor,) + + +@app.cell +def _(Q_, rotor): + campbell = rotor.run_campbell(speed_range=Q_(list(range(0, 4500, 50)), "RPM")) + return (campbell,) + + +@app.cell +def _(campbell): + campbell.plot(frequency_units="RPM") + return + + +@app.cell +def _(Q_, rotor): + modal = rotor.run_modal(speed=Q_(4000, "RPM")) + return (modal,) + + +@app.cell +def _(display, modal): + for _mode in range(6): + display(modal.plot_mode_3d(_mode, frequency_units='Hz')) + return + + +@app.cell +def _(display, modal): + for _mode in range(6): + display(modal.plot_orbit(_mode, nodes=[2, 4])) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_21.rst b/docs/user_guide/example_21.rst new file mode 100644 index 000000000..cd5a5e99e --- /dev/null +++ b/docs/user_guide/example_21.rst @@ -0,0 +1,5 @@ +Example 21 +========== + +.. marimo:: example_21.py + :height: 700px diff --git a/docs/user_guide/example_22.py b/docs/user_guide/example_22.py new file mode 100644 index 000000000..3bf8f21fe --- /dev/null +++ b/docs/user_guide/example_22.py @@ -0,0 +1,128 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 22 - Features of Eigenvalues and Eigenvectors - The Effect o f Axial Load and Follower Torque + + This example is based on Example 5.9.7 from {cite}`friswell2010dynamics`. + + The Effect o f Axial Load and Follower Torque. Investigate the + effect on the model of Example 5.9.1 of axial loads of —10,10, and 100 kN and + torques of 50 and 100 kNm. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import pandas as pd + from IPython.display import display + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return display, pd, rs + + +@app.cell +def _(rs): + Q_ = rs.Q_ + return (Q_,) + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.2e9, rho=7810) + return (steel,) + + +@app.cell +def _(Q_, display, pd, rs, steel): + L = 0.25 + N = 6 + idl = 0 + odl = 0.05 + num_freq = 6 + + bearings = [ + rs.BearingElement(n=0, kxx=1e6, cxx=0, scale_factor=2), + rs.BearingElement(n=N, kxx=1e6, cxx=0, scale_factor=2), + ] + disks = [ + rs.DiskElement.from_geometry( + n=2, material=steel, width=0.07, i_d=odl, o_d=0.28, scale_factor="mass" + ), + rs.DiskElement.from_geometry( + n=4, material=steel, width=0.07, i_d=odl, o_d=0.35, scale_factor="mass" + ), + ] + + results = pd.DataFrame( + {"speed": [0 for _ in range(num_freq)] + [4000 for _ in range(num_freq)]} + ) + + for speed in Q_([0, 4000], "RPM"): + for axial in Q_([0, 10, 100, -10], "kN"): + shaft = [ + rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel, axial_force=axial) + for i in range(N) + ] + rotor = rs.Rotor( + shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings + ) + + modal = rotor.run_modal(speed=speed) + + results.loc[results.speed == speed.m, f"Axial Load {axial.m}"] = ( + Q_(modal.wd[:6], "rad/s").to("Hz").m + ) + + for torque in Q_([50, 100], "kN*m"): + shaft = [ + rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel, torque=torque) + for i in range(N) + ] + rotor = rs.Rotor( + shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings + ) + + modal = rotor.run_modal(speed=speed) + + results.loc[results.speed == speed.m, f"Torque {torque.m}"] = ( + Q_(modal.wd[:6], "rad/s").to("Hz").m + ) + + display(rotor.plot_rotor()) + display(results) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_22.rst b/docs/user_guide/example_22.rst new file mode 100644 index 000000000..eb74b23ae --- /dev/null +++ b/docs/user_guide/example_22.rst @@ -0,0 +1,5 @@ +Example 22 +========== + +.. marimo:: example_22.py + :height: 700px diff --git a/docs/user_guide/example_23.py b/docs/user_guide/example_23.py new file mode 100644 index 000000000..00e82a607 --- /dev/null +++ b/docs/user_guide/example_23.py @@ -0,0 +1,128 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 23 - An Overhung Rotor + + This example is based on Example 5.9.9 from {cite}`friswell2010dynamics`. + + An Overhung Rotor. Consider the overhung rotor shown in Figure 5.44. + The shaft is 1.5m-long and the diameter is 50 mm with a disk of diameter + 350mm and thickness 70 mm. The two bearings, with positions given in + Figure 5.44, have a stiffness of 10 MN/m in each direction. The shaft and disk + are made of steel, with material properties E = 211 GN/m², G = 81.2 GN/m² + and ρ = 7,810 kg/m3. Damping is neglected. Estimate the first six natural + frequencies and mode shapes between 0 and 4,000 rev/min. + """) + return + + +@app.cell +def _(): + import ross as rs + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return (rs,) + + +@app.cell +def _(rs): + Q_ = rs.Q_ + return (Q_,) + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.2e9, rho=7810) + return (steel,) + + +@app.cell +def _(rs, steel): + L = 0.25 + N = 6 + idl = 0 + odl = 0.05 + + shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for i in range(N)] + bearings = [ + rs.BearingElement(n=0, kxx=1e7, cxx=0), + rs.BearingElement(n=4, kxx=1e7, cxx=0), + ] + disks = [ + rs.DiskElement.from_geometry( + n=N, + material=steel, + width=0.07, + i_d=odl, + o_d=0.35, + ), + ] + + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor.plot_rotor() + return (rotor,) + + +@app.cell +def _(Q_, rotor): + campbell = rotor.run_campbell(speed_range=Q_(list(range(0, 4500, 50)), "RPM")) + return (campbell,) + + +@app.cell +def _(campbell): + campbell.plot(frequency_units="RPM", harmonics=[1, 2, 3]) + return + + +@app.cell +def _(Q_, rotor): + modal = rotor.run_modal(speed=Q_(4000, "RPM")) + return (modal,) + + +@app.cell +def _(display, modal): + for _mode in range(6): + display(modal.plot_mode_3d(_mode, frequency_units='Hz')) + return + + +@app.cell +def _(display, modal): + for _mode in range(6): + display(modal.plot_orbit(_mode, nodes=[2, 4])) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_23.rst b/docs/user_guide/example_23.rst new file mode 100644 index 000000000..39b4cff1f --- /dev/null +++ b/docs/user_guide/example_23.rst @@ -0,0 +1,5 @@ +Example 23 +========== + +.. marimo:: example_23.py + :height: 700px diff --git a/docs/user_guide/example_24.py b/docs/user_guide/example_24.py new file mode 100644 index 000000000..ec1c5caf9 --- /dev/null +++ b/docs/user_guide/example_24.py @@ -0,0 +1,121 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 24 - A Tapared Shaft + + This example is based on Example 5.9.10 from {cite}`friswell2010dynamics`. + + A Tapered Shaft. Consider a tapered shaft of length 1.5 m and + a diameter that changes linearly from 25 to 40 mm. A disk of diameter 250 mm + and thickness 40 mm is placed at the center of the shaft, and short bearings + of stiffness 10 MN/m and damping 1 kNs/m are attached at the ends of the + shaft. The Young’s modulus and mass density are 211 GN/mz and 7,810 kg/m3, + respectively. Estimate the first pair of natural frequencies of this machine at + 3,000 rev/min using a stepped shaft diameter and elements of uniform diameter + and by using tapered elements. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + from IPython.display import display + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return display, go, np, rs + + +@app.cell +def _(rs): + Q_ = rs.Q_ + return (Q_,) + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.2e9, rho=7810) + return (steel,) + + +@app.cell +def _(Q_, display, np, rs, steel): + shaft_length = 1.5 + diameter_left = 0.025 + diameter_right = 0.04 + min_elements = 4 + max_elements = 30 + step = 2 + num_simulations = (max_elements - min_elements) // step + results = np.zeros((2, num_simulations)) + results_tapared = np.zeros((2, num_simulations)) + for (_i, N) in enumerate(range(min_elements, max_elements, step)): + L = shaft_length / N + odl_array = np.linspace(diameter_left, diameter_right, N + 1)[:-1] + idl_array = np.zeros_like(odl_array) + odr_array = np.linspace(diameter_left, diameter_right, N + 1)[1:] + idr_array = np.zeros_like(odr_array) + id_array = np.zeros(N) + od_array = np.mean(np.array([odl_array, odr_array]), axis=0) + shaft = [] + shaft_tapared = [] + for n in range(N): + shaft.append(rs.ShaftElement(n=n, L=L, idl=id_array[n], odl=od_array[n], material=steel)) + shaft_tapared.append(rs.ShaftElement(n=n, L=L, idl=idl_array[n], idr=idr_array[n], odl=odl_array[n], odr=odr_array[n], material=steel)) + bearings = [rs.BearingElement(n=0, kxx=10000000.0, cxx=1000.0, scale_factor=2), rs.BearingElement(n=N, kxx=10000000.0, cxx=1000.0, scale_factor=2)] + disks = [rs.DiskElement.from_geometry(n=N // 2, material=steel, width=0.04, i_d=0.0, o_d=0.25)] + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor_tapared = rs.Rotor(shaft_elements=shaft_tapared, disk_elements=disks, bearing_elements=bearings) + modal = rotor.run_modal(speed=Q_(3000, 'RPM')) + modal_tapared = rotor_tapared.run_modal(speed=Q_(3000, 'RPM')) + results[:, _i] = Q_(modal.wd[:2], 'rad/s').to('Hz').m + results_tapared[:, _i] = Q_(modal_tapared.wd[:2], 'rad/s').to('Hz').m + if N == 6: + display(rotor.plot_rotor(nodes=2, title=dict(text='Uniform shaft elements'))) + display(rotor_tapared.plot_rotor(nodes=2, title=dict(text='Tapared shaft elements'))) + return max_elements, min_elements, results, results_tapared, step + + +@app.cell +def _(go, max_elements, min_elements, results, results_tapared, step): + fig = go.Figure() + N_eigen = 2 + for _i in range(N_eigen): + fig.add_trace(go.Scatter(x=list(range(min_elements, max_elements, step)), y=results[_i, :], line=dict(dash='dash'), name=f'Uniform Elements - Mode {_i}')) + fig.add_trace(go.Scatter(x=list(range(min_elements, max_elements, step)), y=results_tapared[_i, :], name=f'Tapared Elements - Mode {_i}')) + fig.update_layout(xaxis=dict(title='Number of Elements'), yaxis=dict(title='Natural Frequency (Hz)')) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_24.rst b/docs/user_guide/example_24.rst new file mode 100644 index 000000000..83d6f389a --- /dev/null +++ b/docs/user_guide/example_24.rst @@ -0,0 +1,5 @@ +Example 24 +========== + +.. marimo:: example_24.py + :height: 700px diff --git a/docs/user_guide/example_25.py b/docs/user_guide/example_25.py new file mode 100644 index 000000000..d97e197f5 --- /dev/null +++ b/docs/user_guide/example_25.py @@ -0,0 +1,237 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 25 - Coaxial rotor + + This example is based on Example 6.8.1 from {cite}`friswell2010dynamics`. + + Consider the model of a simple overhung rotor, 1.5 m long with bearings at 0.0 m and 1.0 m, and shown in Figure 6.49. The bearings are short in that they present insignificant angular stiffness to the shaft, but they present finite translational stiffness. The shaft is 25 mm in diameter and the disk at the overhung end is 250 mm in diameter and 40 mm thick. The shaft and disk are made of steel, and a mass density p = 7,810 kg/m3, a modulus of elasticity E = 211 GPa, and a Poisson’s ratio of 0.3 are assumed. Several different variants of this system are considered in which the differences lie in the bearing (and bearing-support) properties, shown in Table 6.3, and the steady rotational speed of the shaft. Calculate the eigenvalues for various rotor spin speeds and estimate the critical speeds for unbalance excitation. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + import pandas as pd + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + + Q_ = rs.Q_ + rs.__version__ + return Q_, np, pd, rs + + +@app.cell +def _(rs): + # Material + steel = rs.Material("steel", E=211e9, Poisson=0.3, rho=7810) + return (steel,) + + +@app.cell +def _(Q_, rs, steel): + # Cylindrical shaft elements + L = 6 * [250] + i_d = 6 * [0] + o_d = 6 * [25] + shaft_elements = [rs.ShaftElement(n=_i, L=Q_(l, 'mm'), idl=Q_(id1, 'mm'), odl=Q_(od1, 'mm'), material=steel, shear_effects=True, rotary_inertia=True, gyroscopic=True) for (_i, l, id1, od1) in zip(range(len(L)), L, i_d, o_d)] + print('Number os shaft elements: %d.' % len(shaft_elements)) + return (shaft_elements,) + + +@app.cell +def _(Q_, rs): + # Creating a disk element + disk_elements = [ + rs.DiskElement( + n=6, + m=Q_(15.3, "kg"), + Id=Q_(0.062, "kg * m**2"), + Ip=Q_(0.120, "kg * m**2"), + scale_factor=6, + ) + ] + return (disk_elements,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Characteristic roots computed for a number of configurations + """) + return + + +@app.cell +def _(Q_, disk_elements, np, pd, rs, shaft_elements): + """ + Table 6.3 - Bearing and Support characteristics + """ + cases = {1: [10, 10, 0, 0], 2: [10, 20, 0, 0], 3: [10, 20, 60, 60], 4: [10, 20, 400, 400], 5: [0.2, 0.4, 0, 0]} + '\n Bearing configurations from table 6.4\n' + configurations = [[0, 1, 1], [0, 2, 2], [0, 2, 3], [0, 2, 4], [0, 5, 5], [1000.0, 1, 1], [1000.0, 2, 3], [2000.0, 2, 3], [3000.0, 2, 3]] + data = [] + bearing_pairs = [] + pd.options.display.float_format = '{:.3f}'.format + for _i in configurations: + (speed, left, rigth) = _i + bearing_elements = [rs.BearingElement(n=0, kxx=cases[left][0] * 1000000.0, kyy=cases[left][1] * 1000000.0, cxx=cases[left][2] * 1000.0, cyy=cases[left][3] * 1000.0, scale_factor=6), rs.BearingElement(n=4, kxx=cases[rigth][0] * 1000000.0, kyy=cases[rigth][1] * 1000000.0, cxx=cases[rigth][2] * 1000.0, cyy=cases[rigth][3] * 1000.0, scale_factor=6)] + rotor = rs.Rotor(shaft_elements, disk_elements, bearing_elements) + bearing_pairs.append(bearing_elements) + _modal = rotor.run_modal(speed=Q_(speed, 'RPM')) + roots = [] + for root in _modal.evalues: + if np.iscomplex(root): + roots.append(root) + if len(roots) == 4: + break + data.append({('Config:', 'Speed'): speed, ('Config:', 'Left'): left, ('Config:', 'Rigth'): rigth, ('Roots:', 'First'): roots[0], ('Roots:', 'Second'): roots[1], ('Roots:', 'Third'): roots[2], ('Roots:', 'Fourth'): roots[3]}) + multi_index = pd.MultiIndex.from_tuples([('Config:', 'Speed'), ('Config:', 'Left'), ('Config:', 'Rigth'), ('Roots:', 'First'), ('Roots:', 'Second'), ('Roots:', 'Third'), ('Roots:', 'Fourth')]) + df = pd.DataFrame(data, columns=multi_index) + # Create empty lists to store data and bearing pairs + # Configure Pandas to display only three decimal places + # Use a for loop to generate data + # Convert the list of dictionaries to a DataFrame + # Display the DataFrame + df # Bearings instances # Saving bearing pairs for later analysis # Performing modal analysis # Selection of the first 4 complex roots + return (bearing_pairs,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Rotor Model + """) + return + + +@app.cell +def _(bearing_pairs, disk_elements, np, rs, shaft_elements): + rotor_1 = rs.Rotor(shaft_elements, disk_elements, bearing_pairs[4]) + print('ROTOR DATA:\nRotor total mass = ', np.round(rotor_1.m, 2)) + print('Rotor center of gravity =', np.round(rotor_1.CG, 2)) + rotor_1.plot_rotor(width=500, height=400) + return (rotor_1,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Campbell Diagram + + The following chart presents the Campbell diagram for a system where both the left and right bearings exhibit the characteristics of Case 5, as detailed in Table 6.3 on page 279 of [Fryswell, 2010](#References). + + The chart can be found in page 280 of the reference. + """) + return + + +@app.cell +def _(np, rotor_1): + samples = 41 + max_spin = 400 + speed_range = np.linspace(0, max_spin, samples) + campbell = rotor_1.run_campbell(speed_range, frequency_type='wn') + _fig = campbell.plot(frequency_units='rpm', width=600, height=600) + for _i in _fig.data: + try: + _i['y'] = _i['y'] / 60 + except: + pass + _fig.update_yaxes(title='Natural Frequencies (Hz)', range=[0, 90]) + _fig.update_xaxes(title='Rotor Spin Speed (rpm)', range=[0, max_spin * 60 / (2 * np.pi)]) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Modal Shapes + + Mode shapes for the critical speeds for the onverhung rotor. This example is based on Example 6.9.1 page 281 from [Fryswell, 2010](#References). + + Find the critical speeds and associated mode shapes for the + system defined in Example 6.8.1 with bearing ccharacteristic case 5, defined in + Table 6.3. + """) + return + + +@app.cell +def _(Q_, rotor_1): + _modal = rotor_1.run_modal(speed=Q_(358.43, 'RPM')) + _fig = _modal.plot_mode_3d(0, frequency_units='rpm', width=600, height=600) + _fig.layout.title.text = 'FIRST CRITICAL SPEED' + _fig.layout.title.text + _fig + return + + +@app.cell +def _(Q_, rotor_1): + _modal = rotor_1.run_modal(speed=Q_(389.35, 'RPM')) + _fig = _modal.plot_mode_3d(1, frequency_units='rpm', width=600, height=600) + _fig.layout.title.text = 'SECOND CRITICAL SPEED' + _fig.layout.title.text + _fig + return + + +@app.cell +def _(Q_, rotor_1): + _modal = rotor_1.run_modal(speed=Q_(2123.79, 'RPM')) + _fig = _modal.plot_mode_3d(2, frequency_units='rpm', width=600, height=600) + _fig.layout.title.text = 'THIRD CRITICAL SPEED' + _fig.layout.title.text + _fig + return + + +@app.cell +def _(Q_, rotor_1): + _modal = rotor_1.run_modal(speed=Q_(2659.1, 'RPM')) + _fig = _modal.plot_mode_3d(3, frequency_units='rpm', width=600, height=600) + _fig.layout.title.text = 'FOURTH CRITICAL SPEED' + _fig.layout.title.text + _fig + return + + +@app.cell +def _(Q_, rotor_1): + _modal = rotor_1.run_modal(speed=Q_(3118.14, 'RPM')) + _fig = _modal.plot_mode_3d(4, frequency_units='rpm', width=600, height=600) + _fig.layout.title.text = 'FIFHT CRITICAL SPEED' + _fig.layout.title.text + _fig + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_25.rst b/docs/user_guide/example_25.rst new file mode 100644 index 000000000..94ae37bbe --- /dev/null +++ b/docs/user_guide/example_25.rst @@ -0,0 +1,5 @@ +Example 25 +========== + +.. marimo:: example_25.py + :height: 700px diff --git a/docs/user_guide/example_26.py b/docs/user_guide/example_26.py new file mode 100644 index 000000000..4ccb65776 --- /dev/null +++ b/docs/user_guide/example_26.py @@ -0,0 +1,444 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Example 26 - Isotropic System + + This example is based on Example 6.3.1.a page 253 from {cite}`friswell2010dynamics`. + + The rotor system shown in Figure 6.18 consists of a shaft 1.5 m long with a 50 mm diameter supported by bearings at each end. Disks are mounted on the shaft at one-third and two-third spans. Each disk is 70 mm thick and the left and right disks are 280 and 350 mm in diameter, respectively. The shaft and disks have material properties E — 211 GPa, G = 81.1 GPa, and p — 7,810 kg/m3. Determine the response of the system at the disks due to an out-of-balance on the left disk of 0.001 kgm, if each bearing has a stiffness of 1 MN/m and a damping of 100 Ns/m in both the x and y directions. The natural frequencies and mode shapes for this rotor system are calculated. + """) + return + + +@app.cell +def _(): + import ross as rs + import plotly.graph_objects as go + import plotly.io as pio + import numpy as np + import matplotlib.pyplot as plt + from matplotlib.ticker import ScalarFormatter + pio.renderers.default = 'notebook' + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + Q_ = rs.Q_ + return Q_, ScalarFormatter, go, np, plt, rs + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.1e9, rho=7810) + return (steel,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Defining bearings, shaft and disk elements and Creating isotropic rotor + """) + return + + +@app.cell +def _(rs, steel): + N = 6 + L = 1.5 / N + idl = 0 + odl = 0.05 # shaft diameter + shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for _i in range(N)] + bearings = [rs.BearingElement(n=0, kxx=1000000.0, kyy=1000000.0, cxx=100, cyy=100), rs.BearingElement(n=6, kxx=1000000.0, kyy=1000000.0, cxx=100, cyy=100)] + disks = [rs.DiskElement.from_geometry(n=N / 3, material=steel, width=0.07, i_d=odl, o_d=0.28, scale_factor='mass'), rs.DiskElement.from_geometry(n=2 * N / 3, material=steel, width=0.07, i_d=odl, o_d=0.35, scale_factor='mass')] + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor.plot_rotor(width=750, height=500) + return (rotor,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Plotting the Campbell Diagram + """) + return + + +@app.cell +def _(Q_, np, rotor): + campbell = rotor.run_campbell( + speed_range=Q_(np.linspace(0, 6500, 65), "RPM"), frequencies=7 + ) + return (campbell,) + + +@app.cell +def _(campbell): + fig = campbell.plot(frequency_units='RPM', width=600, height=600) + for _i in fig.data: + try: + _i['y'] = _i['y'] / 60 + except: + pass + fig.update_yaxes(title='Natural Frequencies (Hz)', range=[0, 150]) + fig.update_xaxes(title='Rotor Spin Speed (rpm)', range=[0, 6500]) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Plotting the Mode Shapes and Damped Natural Frequencies + """) + return + + +@app.cell +def _(Q_, rotor): + _speed = Q_(3000, 'RPM') + modal = rotor.run_modal(_speed, num_modes=14) + for _i in range(7): + modal.plot_mode_3d(mode=_i, frequency_units='Hz', damping_parameter='damping_ratio').show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Creating the out-of-balancing + """) + return + + +@app.cell +def _(Q_, np, rotor): + n1 = 2 # out-of-balancing is positioned at the left disk + m1 = 0.001 # amount of out-of-balancing expressed in kg*m + p1 = 0 # ou-of-balancing mass phase position + _speed = Q_(np.linspace(0, 4500, 2451), 'RPM') + results_case1 = rotor.run_unbalance_response([n1], [m1], [p1], _speed) + return m1, results_case1 + + +@app.cell +def _(Q_, results_case1): + _probe1 = (2, 0) + _probe2 = (2, Q_(90, 'deg')) + fig_1 = results_case1.plot(probe=[_probe1, _probe2], probe_units='degrees', frequency_units='RPM', amplitude_units='µm pkpk', phase_units='degrees') + fig_1.update_layout(yaxis=dict(type='log')) + return + + +@app.cell +def _(Q_, results_case1): + _probe1 = (4, 0) + _probe2 = (4, Q_(90, 'deg')) + fig_2 = results_case1.plot(probe=[_probe1, _probe2], probe_units='degrees', frequency_units='RPM', amplitude_units='µm pkpk', phase_units='degrees') + return (fig_2,) + + +@app.cell +def _(fig_2): + # changing to log scale + fig_2.update_layout(yaxis=dict(type='log')) # title='Amplitude (µm pkpk)', # range=[-9, -1] # log range: 10^-9, 10^-1 + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Plotting the Orbit using the Plotly library + """) + return + + +@app.cell +def _(): + #### Building the orbit at 496 RPM, 1346 RPM and 2596 RPM for nodes located at + #### the right and left disks (node=2 and node=4) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### At 496 RPM + """) + return + + +@app.cell +def _(m1, np, rotor): + _speed = 496 * (2 * np.pi / 60) # Q_(496, "RPM") + time_samples = 1000001 + _node = 2 # out-of-balancing position + _t = np.linspace(0, 43, time_samples) + _F = np.zeros((time_samples, rotor.ndof)) + # Creating the out-of-balancing force input matrix + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + # harmonic force component on x axis + # harmonic force component on y axis + # Using the ROSS’ method to calculate displacements due a force in time domain: run_time_response(). + response3 = rotor.run_time_response(_speed, _F, _t) # as out-of-balancing is a harmonic force + return (response3,) + + +@app.cell +def _(go, np, response3): + # Editing the ross plots in order to explicit the orbit whirl in node 2 + _orb2 = response3.plot_2d(node=2, width=500, height=500) + _cutoff = int(1000 * 2.7) + _x_new2 = _orb2.data[0].x[-_cutoff:] + _y_new2 = _orb2.data[0].y[-_cutoff:] + _starting_point2 = go.Scatter(x=[_x_new2[0]], y=[_y_new2[0]], marker={'size': 10, 'color': 'orange'}, showlegend=False, name='Starting Point2') + _orb2_curve = go.Scatter(x=_x_new2, y=_y_new2, mode='lines', name='orb2', showlegend=False, line=dict(color='orange')) + # Inserting the orbit starting point + _orb4 = response3.plot_2d(node=4, width=500, height=500) + _x_new4 = _orb4.data[0].x[-_cutoff:] + _y_new4 = _orb4.data[0].y[-_cutoff:] + _starting_point4 = go.Scatter(x=[_x_new4[0]], y=[_y_new4[0]], marker={'size': 10, 'color': '#636EFA'}, showlegend=False, name='Starting Point4') + _max_amp = max(np.max(_x_new2), np.max(_y_new2), np.max(_x_new4), np.max(_y_new4)) + _orb4.update_xaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_yaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_traces(x=_x_new4, y=_y_new4, name='orb4') + # Inserting the orbit of node 2 + _orb4.update_layout(title='Response at node 2 and 4 at 496 RPM') + _orb4.add_trace(_starting_point4) + _orb4.add_trace(_starting_point2) + _orb4.add_trace(_orb2_curve) + # Editing the ross plots in order to explicit the orbit whirl in node 4 + # Proper scaling x and y axis + # Merging orbit at node 2 and node 4 + _orb4 + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### At 1346 RPM + """) + return + + +@app.cell +def _(m1, np, rotor): + _speed = 1346 * (2 * np.pi / 60) + time_samples_1 = 1000001 + _node = 2 + _t = np.linspace(0, 43, time_samples_1) + _F = np.zeros((time_samples_1, rotor.ndof)) + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + response3_1 = rotor.run_time_response(_speed, _F, _t) + return (response3_1,) + + +@app.cell +def _(go, np, response3_1): + _orb2 = response3_1.plot_2d(node=2, width=500, height=500) + _cutoff = int(1000 * 1) + _x_new2 = _orb2.data[0].x[-_cutoff:] + _y_new2 = _orb2.data[0].y[-_cutoff:] + _starting_point2 = go.Scatter(x=[_x_new2[0]], y=[_y_new2[0]], marker={'size': 10, 'color': 'orange'}, showlegend=False, name='Starting Point2') + _orb2_curve = go.Scatter(x=_x_new2, y=_y_new2, mode='lines', name='orb2', showlegend=False, line=dict(color='orange')) + _orb4 = response3_1.plot_2d(node=4, width=500, height=500) + _x_new4 = _orb4.data[0].x[-_cutoff:] + _y_new4 = _orb4.data[0].y[-_cutoff:] + _starting_point4 = go.Scatter(x=[_x_new4[0]], y=[_y_new4[0]], marker={'size': 10, 'color': '#636EFA'}, showlegend=False, name='Starting Point4') + _max_amp = max(np.max(_x_new2), np.max(_y_new2), np.max(_x_new4), np.max(_y_new4)) + _orb4.update_xaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_yaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_traces(x=_x_new4, y=_y_new4, name='orb4') + _orb4.update_layout(title='Response at node 2 and 4 at 1346 RPM') + _orb4.add_trace(_starting_point4) + _orb4.add_trace(_starting_point2) + _orb4.add_trace(_orb2_curve) + _orb4 + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### At 2596 RPM + """) + return + + +@app.cell +def _(m1, np, rotor): + _speed = 2596 * (2 * np.pi / 60) + time_samples_2 = 1000001 + _node = 2 + _t = np.linspace(0, 43, time_samples_2) + _F = np.zeros((time_samples_2, rotor.ndof)) + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + response3_2 = rotor.run_time_response(_speed, _F, _t) + return (response3_2,) + + +@app.cell +def _(go, np, response3_2): + _orb2 = response3_2.plot_2d(node=2, width=500, height=500) + _cutoff = int(1000 * 0.52) + _x_new2 = _orb2.data[0].x[-_cutoff:] + _y_new2 = _orb2.data[0].y[-_cutoff:] + _starting_point2 = go.Scatter(x=[_x_new2[0]], y=[_y_new2[0]], marker={'size': 10, 'color': 'orange'}, showlegend=False, name='Starting Point2') + _orb2_curve = go.Scatter(x=_x_new2, y=_y_new2, mode='lines', name='orb2', showlegend=False, line=dict(color='orange')) + _orb4 = response3_2.plot_2d(node=4, width=500, height=500) + _x_new4 = _orb4.data[0].x[-_cutoff:] + _y_new4 = _orb4.data[0].y[-_cutoff:] + _starting_point4 = go.Scatter(x=[_x_new4[0]], y=[_y_new4[0]], marker={'size': 10, 'color': '#636EFA'}, showlegend=False, name='Starting Point4') + _max_amp = max(np.max(_x_new2), np.max(_y_new2), np.max(_x_new4), np.max(_y_new4)) + _orb4.update_xaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_yaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_traces(x=_x_new4, y=_y_new4, name='orb4') + _orb4.update_layout(title='Response at node 2 and 4 at 2596 RPM') + _orb4.add_trace(_starting_point4) + _orb4.add_trace(_starting_point2) + _orb4.add_trace(_orb2_curve) + _orb4 + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Plotting the Orbit using the Matplotlib library + """) + return + + +@app.cell +def _(m1, np, rotor): + _speed = 496 * (2 * np.pi / 60) + time_samples_3 = 1000001 + _node = 2 + _t = np.linspace(0, 43, time_samples_3) + _F = np.zeros((time_samples_3, rotor.ndof)) + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + response3_3 = rotor.run_time_response(_speed, _F, _t) + node_response = 2 + x1_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y1_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + t_vector = response3_3.t + node_response = 4 + x2_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y2_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + _speed = 1346 * (2 * np.pi / 60) + time_samples_3 = 1000001 + _node = 2 + _t = np.linspace(0, 43, time_samples_3) + _F = np.zeros((time_samples_3, rotor.ndof)) + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + response3_3 = rotor.run_time_response(_speed, _F, _t) + node_response = 2 + x3_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y3_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + node_response = 4 + x4_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y4_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + _speed = 2596 * (2 * np.pi / 60) + time_samples_3 = 1000001 + _node = 2 + _t = np.linspace(0, 43, time_samples_3) + _F = np.zeros((time_samples_3, rotor.ndof)) + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + response3_3 = rotor.run_time_response(_speed, _F, _t) + node_response = 2 + x5_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y5_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + node_response = 4 + x6_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y6_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + return ( + time_samples_3, + x1_axis_displacement, + x2_axis_displacement, + x3_axis_displacement, + x4_axis_displacement, + x5_axis_displacement, + x6_axis_displacement, + y1_axis_displacement, + y2_axis_displacement, + y3_axis_displacement, + y4_axis_displacement, + y5_axis_displacement, + y6_axis_displacement, + ) + + +@app.cell +def _( + ScalarFormatter, + plt, + time_samples_3, + x1_axis_displacement, + x2_axis_displacement, + x3_axis_displacement, + x4_axis_displacement, + x5_axis_displacement, + x6_axis_displacement, + y1_axis_displacement, + y2_axis_displacement, + y3_axis_displacement, + y4_axis_displacement, + y5_axis_displacement, + y6_axis_displacement, +): + (fig_3, (ax1, ax2, ax3)) = plt.subplots(1, 3, figsize=(13, 4)) + formatter = ScalarFormatter(useMathText=True) + formatter.set_powerlimits((-6, -6)) + _cutoff = -int(time_samples_3 * 0.0027) + ax1.plot(x1_axis_displacement[_cutoff:], y1_axis_displacement[_cutoff:], label='Orbit') + ax1.plot(x1_axis_displacement[_cutoff:][0], y1_axis_displacement[_cutoff:][0], 'o', markersize=6, color='#636EFA') + ax1.plot(x2_axis_displacement[_cutoff:], y2_axis_displacement[_cutoff:], label='Orbit') + ax1.plot(x2_axis_displacement[_cutoff:][0], y2_axis_displacement[_cutoff:][0], 'o', markersize=6, color='#636EFA') + ax1.set_title('496 RPM') + ax1.xaxis.set_major_formatter(formatter) + ax1.yaxis.set_major_formatter(formatter) + _cutoff = -int(time_samples_3 * 0.001) + ax2.plot(x3_axis_displacement[_cutoff:], y3_axis_displacement[_cutoff:], label='Orbit') + ax2.plot(x3_axis_displacement[_cutoff:][0], y3_axis_displacement[_cutoff:][0], 'o', markersize=10, color='#636EFA') + ax2.plot(x4_axis_displacement[_cutoff:], y4_axis_displacement[_cutoff:], label='Orbit') + ax2.plot(x4_axis_displacement[_cutoff:][0], y4_axis_displacement[_cutoff:][0], 'o', markersize=10, color='#636EFA') + ax2.set_title('1346 RPM') + ax2.xaxis.set_major_formatter(formatter) + ax2.yaxis.set_major_formatter(formatter) + _cutoff = -int(time_samples_3 * 0.00052) + ax3.plot(x5_axis_displacement[_cutoff:], y5_axis_displacement[_cutoff:], label='Orbit') + ax3.plot(x5_axis_displacement[_cutoff:][0], y5_axis_displacement[_cutoff:][0], 'o', markersize=10, color='#636EFA') + ax3.plot(x6_axis_displacement[_cutoff:], y6_axis_displacement[_cutoff:], label='Orbit') + ax3.plot(x6_axis_displacement[_cutoff:][0], y6_axis_displacement[_cutoff:][0], 'o', markersize=10, color='#636EFA') + ax3.set_title('2596 RPM') + ax3.xaxis.set_major_formatter(formatter) + ax3.yaxis.set_major_formatter(formatter) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### REFERENCES + [1] M. I. Friswell, J. E. T. Penny, S. D. Garvey, and A. W. Lees, Dynamics of Rotating Machines. Cambridge: Cambridge University Press, 2010. + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_26.rst b/docs/user_guide/example_26.rst new file mode 100644 index 000000000..c7fe96a63 --- /dev/null +++ b/docs/user_guide/example_26.rst @@ -0,0 +1,5 @@ +Example 26 +========== + +.. marimo:: example_26.py + :height: 700px diff --git a/docs/user_guide/example_27.py b/docs/user_guide/example_27.py new file mode 100644 index 000000000..59af6cf19 --- /dev/null +++ b/docs/user_guide/example_27.py @@ -0,0 +1,466 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Example 27 - Anisotropic System + + This example is based on Example 6.3.1.b page 253 from {cite}`friswell2010dynamics`. + + The rotor system shown in Figure 6.18 consists of a shaft 1.5 m long with a 50 mm diameter supported by bearings at each end. Disks are mounted on the shaft at one-third and two-third spans. Each disk is 70 mm thick and the left and right disks are 280 and 350 mm in diameter, respectively. The shaft and disks have material properties E — 211 GPa, G = 81.1 GPa, and p — 7,810 kg/m3. Determine the response of the system at the disks due to an out-of-balance on the left disk of 0.001 kgm, if each bearing has a stiffness of 1 MN/m in the x direction and 0.8 MN/m in the y direction and a damping of 100 Ns/m in both directions. The natural frequencies and mode shapes for these rotor systems are calculated in Examples 5.9.1 and 5.9.2. + """) + return + + +@app.cell +def _(): + import ross as rs + import plotly.graph_objects as go + import plotly.io as pio + import numpy as np + import matplotlib.pyplot as plt + from matplotlib.ticker import ScalarFormatter + pio.renderers.default = 'notebook' + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + Q_ = rs.Q_ + return Q_, ScalarFormatter, go, np, plt, rs + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.1e9, rho=7810) + return (steel,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Defining bearings, shaft and disk elements and Creating Anisotropic Rotor + """) + return + + +@app.cell +def _(rs, steel): + N = 6 + L = 1.5 / N + idl = 0 + odl = 0.05 # shaft diameter + shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for _i in range(N)] + bearings = [rs.BearingElement(n=0, kxx=1000000.0, kyy=800000.0, cxx=100, cyy=100), rs.BearingElement(n=6, kxx=1000000.0, kyy=800000.0, cxx=100, cyy=100)] + disks = [rs.DiskElement.from_geometry(n=N / 3, material=steel, width=0.07, i_d=odl, o_d=0.28, scale_factor='mass'), rs.DiskElement.from_geometry(n=2 * N / 3, material=steel, width=0.07, i_d=odl, o_d=0.35, scale_factor='mass')] + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor.plot_rotor(width=750, height=500) + return (rotor,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Plotting the Campbell Diagram + """) + return + + +@app.cell +def _(Q_, np, rotor): + campbell = rotor.run_campbell( + speed_range=Q_(np.linspace(0, 6500, 65), "RPM"), frequencies=7 + ) + return (campbell,) + + +@app.cell +def _(campbell): + fig = campbell.plot(frequency_units='RPM', width=600, height=600) + for _i in fig.data: + try: + _i['y'] = _i['y'] / 60 + except: + pass + fig.update_yaxes(title='Natural Frequencies (Hz)', range=[0, 150]) + fig.update_xaxes(title='Rotor Spin Speed (rpm)', range=[0, 6500]) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Plotting the Mode Shapes and Damped Natural Frequencies + """) + return + + +@app.cell +def _(Q_, rotor): + _speed = Q_(3000, 'RPM') + modal = rotor.run_modal(_speed, num_modes=14) + for _i in range(7): + modal.plot_mode_3d(mode=_i, frequency_units='Hz', damping_parameter='damping_ratio').show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Creating the out-of-balancing + """) + return + + +@app.cell +def _(Q_, np, rotor): + _n1 = 2 # out-of-balancing is positioned at the left disk + m1 = 0.001 # amount of out-of-balancing expressed in kg*m + _p1 = 0 # ou-of-balancing mass phase position + _speed = Q_(np.linspace(10, 4500, 1950), 'RPM') + results_case1 = rotor.run_unbalance_response([_n1], [m1], [_p1], _speed) + return m1, results_case1 + + +@app.cell +def _(Q_, results_case1): + _probe1 = (2, 0) + _probe2 = (2, Q_(90, 'deg')) + fig_1 = results_case1.plot_magnitude(probe=[_probe1, _probe2], probe_units='degrees', frequency_units='RPM', amplitude_units='µm pkpk') + fig_1.update_layout(yaxis=dict(type='log')) + return + + +@app.cell +def _(Q_, results_case1): + _probe1 = (4, 0) + _probe2 = (4, Q_(90, 'deg')) + fig_2 = results_case1.plot_magnitude(probe=[_probe1, _probe2], probe_units='degrees', frequency_units='RPM', amplitude_units='µm pkpk') + return (fig_2,) + + +@app.cell +def _(fig_2): + # changing to log scale + fig_2.update_layout(yaxis=dict(type='log')) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Plotting the Orbit using the Plotly library + """) + return + + +@app.cell +def _(): + #### Building the orbit at 751 RPM, 800 RPM and 2320 RPM for nodes located at + #### the right and left disks (node=2 and node=4) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### At 751 RPM + """) + return + + +@app.cell +def _(m1, np, rotor): + _speed = 751 * (2 * np.pi / 60) # Q_(496, "RPM") + time_samples = 1000001 + _node = 2 # out-of-balancing position + _t = np.linspace(0, 43, time_samples) + _F = np.zeros((time_samples, rotor.ndof)) + # Creating the out-of-balancing force input matrix + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + # harmonic force component on x axis + # harmonic force component on y axis + # Using the ROSS’ method to calculate displacements due a force in time domain: run_time_response(). + response3 = rotor.run_time_response(_speed, _F, _t) # as out-of-balancing is a harmonic force + return (response3,) + + +@app.cell +def _(go, np, response3): + # Editing the ross plots in order to explicit the orbit whirl in node 2 + _orb2 = response3.plot_2d(node=2, width=500, height=500) + _cutoff = int(1000 * 1.8) + _x_new2 = _orb2.data[0].x[-_cutoff:] + _y_new2 = _orb2.data[0].y[-_cutoff:] + _starting_point2 = go.Scatter(x=[_x_new2[0]], y=[_y_new2[0]], marker={'size': 10, 'color': 'orange'}, showlegend=False, name='Starting Point2') + _orb2_curve = go.Scatter(x=_x_new2, y=_y_new2, mode='lines', name='orb2', showlegend=False, line=dict(color='orange')) + # Inserting the orbit starting point + _orb4 = response3.plot_2d(node=4, width=500, height=500) + _x_new4 = _orb4.data[0].x[-_cutoff:] + _y_new4 = _orb4.data[0].y[-_cutoff:] + _starting_point4 = go.Scatter(x=[_x_new4[0]], y=[_y_new4[0]], marker={'size': 10, 'color': '#636EFA'}, showlegend=False, name='Starting Point4') + _max_amp = max(np.max(_x_new2), np.max(_y_new2), np.max(_x_new4), np.max(_y_new4)) + _orb4.update_xaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_yaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_traces(x=_x_new4, y=_y_new4, name='orb4') + # Inserting the orbit of node 2 + _orb4.update_layout(title='Response at node 2 and 4 at 751 RPM') + _orb4.add_trace(_starting_point4) + _orb4.add_trace(_starting_point2) + _orb4.add_trace(_orb2_curve) + # Editing the ross plots in order to explicit the orbit whirl in node 4 + # Proper scaling x and y axis + # Merging orbit at node 2 and node 4 + _orb4 + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### At 800 RPM + """) + return + + +@app.cell +def _(m1, np, rotor): + _speed = 800 * (2 * np.pi / 60) + time_samples_1 = 1000001 + _node = 2 + _t = np.linspace(0, 43, time_samples_1) + _F = np.zeros((time_samples_1, rotor.ndof)) + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + response3_1 = rotor.run_time_response(_speed, _F, _t) + return (response3_1,) + + +@app.cell +def _(go, np, response3_1): + _orb2 = response3_1.plot_2d(node=2, width=500, height=500) + _cutoff = int(1000 * 1.7) + _x_new2 = _orb2.data[0].x[-_cutoff:] + _y_new2 = _orb2.data[0].y[-_cutoff:] + _starting_point2 = go.Scatter(x=[_x_new2[0]], y=[_y_new2[0]], marker={'size': 10, 'color': 'orange'}, showlegend=False, name='Starting Point2') + _orb2_curve = go.Scatter(x=_x_new2, y=_y_new2, mode='lines', name='orb2', showlegend=False, line=dict(color='orange')) + _orb4 = response3_1.plot_2d(node=4, width=500, height=500) + _x_new4 = _orb4.data[0].x[-_cutoff:] + _y_new4 = _orb4.data[0].y[-_cutoff:] + _starting_point4 = go.Scatter(x=[_x_new4[0]], y=[_y_new4[0]], marker={'size': 10, 'color': '#636EFA'}, showlegend=False, name='Starting Point4') + _max_amp = max(np.max(_x_new2), np.max(_y_new2), np.max(_x_new4), np.max(_y_new4)) + _orb4.update_xaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_yaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_traces(x=_x_new4, y=_y_new4, name='orb4') + _orb4.update_layout(title='Response at node 2 and 4 at 800 RPM') + _orb4.add_trace(_starting_point4) + _orb4.add_trace(_starting_point2) + _orb4.add_trace(_orb2_curve) + _orb4 + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### At 2320 RPM + """) + return + + +@app.cell +def _(m1, np, rotor): + _speed = 2320 * (2 * np.pi / 60) + time_samples_2 = 1000001 + _node = 2 + _t = np.linspace(0, 43, time_samples_2) + _F = np.zeros((time_samples_2, rotor.ndof)) + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + response3_2 = rotor.run_time_response(_speed, _F, _t) + return (response3_2,) + + +@app.cell +def _(go, np, response3_2): + _orb2 = response3_2.plot_2d(node=2, width=500, height=500) + _cutoff = int(1000 * 0.57) + _x_new2 = _orb2.data[0].x[-_cutoff:] + _y_new2 = _orb2.data[0].y[-_cutoff:] + _starting_point2 = go.Scatter(x=[_x_new2[0]], y=[_y_new2[0]], marker={'size': 10, 'color': 'orange'}, showlegend=False, name='Starting Point2') + _orb2_curve = go.Scatter(x=_x_new2, y=_y_new2, mode='lines', name='orb2', showlegend=False, line=dict(color='orange')) + _orb4 = response3_2.plot_2d(node=4, width=500, height=500) + _x_new4 = _orb4.data[0].x[-_cutoff:] + _y_new4 = _orb4.data[0].y[-_cutoff:] + _starting_point4 = go.Scatter(x=[_x_new4[0]], y=[_y_new4[0]], marker={'size': 10, 'color': '#636EFA'}, showlegend=False, name='Starting Point4') + _max_amp = max(np.max(_x_new2), np.max(_y_new2), np.max(_x_new4), np.max(_y_new4)) + _orb4.update_xaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_yaxes(range=[-1.2 * _max_amp, 1.2 * _max_amp]) + _orb4.update_traces(x=_x_new4, y=_y_new4, name='orb4') + _orb4.update_layout(title='Response at node 2 and 4 at 2320 RPM') + _orb4.add_trace(_starting_point4) + _orb4.add_trace(_starting_point2) + _orb4.add_trace(_orb2_curve) + _orb4 + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Plotting the Orbit using the Matplotlib library + """) + return + + +@app.cell +def _(m1, np, rotor): + _speed = 751 * (2 * np.pi / 60) + time_samples_3 = 1000001 + _node = 2 + _t = np.linspace(0, 43, time_samples_3) + _F = np.zeros((time_samples_3, rotor.ndof)) + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + response3_3 = rotor.run_time_response(_speed, _F, _t) + node_response = 2 + x1_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y1_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + t_vector = response3_3.t + node_response = 4 + x2_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y2_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + _speed = 800 * (2 * np.pi / 60) + time_samples_3 = 1000001 + _node = 2 + _t = np.linspace(0, 43, time_samples_3) + _F = np.zeros((time_samples_3, rotor.ndof)) + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + response3_3 = rotor.run_time_response(_speed, _F, _t) + node_response = 2 + x3_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y3_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + node_response = 4 + x4_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y4_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + _speed = 2320 * (2 * np.pi / 60) + time_samples_3 = 1000001 + _node = 2 + _t = np.linspace(0, 43, time_samples_3) + _F = np.zeros((time_samples_3, rotor.ndof)) + _F[:, rotor.number_dof * _node + 0] = m1 * _speed ** 2 * np.cos(_speed * _t) + _F[:, rotor.number_dof * _node + 1] = m1 * _speed ** 2 * np.sin(_speed * _t) + response3_3 = rotor.run_time_response(_speed, _F, _t) + node_response = 2 + x5_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y5_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + node_response = 4 + x6_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 0] + y6_axis_displacement = response3_3.yout[:, rotor.number_dof * node_response + 1] + return ( + time_samples_3, + x1_axis_displacement, + x2_axis_displacement, + x3_axis_displacement, + x4_axis_displacement, + x5_axis_displacement, + x6_axis_displacement, + y1_axis_displacement, + y2_axis_displacement, + y3_axis_displacement, + y4_axis_displacement, + y5_axis_displacement, + y6_axis_displacement, + ) + + +@app.cell +def _( + ScalarFormatter, + plt, + time_samples_3, + x1_axis_displacement, + x2_axis_displacement, + x3_axis_displacement, + x4_axis_displacement, + x5_axis_displacement, + x6_axis_displacement, + y1_axis_displacement, + y2_axis_displacement, + y3_axis_displacement, + y4_axis_displacement, + y5_axis_displacement, + y6_axis_displacement, +): + (fig_3, (ax1, ax2, ax3)) = plt.subplots(1, 3, figsize=(13, 4)) + formatter = ScalarFormatter(useMathText=True) + formatter.set_powerlimits((-6, -6)) + _cutoff = -int(time_samples_3 * 0.0018) + ax1.plot(x1_axis_displacement[_cutoff:], y1_axis_displacement[_cutoff:], label='Orbit') + ax1.plot(x1_axis_displacement[_cutoff:][0], y1_axis_displacement[_cutoff:][0], 'o', markersize=6, color='#636EFA') + ax1.plot(x2_axis_displacement[_cutoff:], y2_axis_displacement[_cutoff:], label='Orbit') + ax1.plot(x2_axis_displacement[_cutoff:][0], y2_axis_displacement[_cutoff:][0], 'o', markersize=6, color='#636EFA') + ax1.set_title('496 RPM') + ax1.xaxis.set_major_formatter(formatter) + ax1.yaxis.set_major_formatter(formatter) + _cutoff = -int(time_samples_3 * 0.0017) + ax2.plot(x3_axis_displacement[_cutoff:], y3_axis_displacement[_cutoff:], label='Orbit') + ax2.plot(x3_axis_displacement[_cutoff:][0], y3_axis_displacement[_cutoff:][0], 'o', markersize=10, color='#636EFA') + ax2.plot(x4_axis_displacement[_cutoff:], y4_axis_displacement[_cutoff:], label='Orbit') + ax2.plot(x4_axis_displacement[_cutoff:][0], y4_axis_displacement[_cutoff:][0], 'o', markersize=10, color='#636EFA') + ax2.set_title('1346 RPM') + ax2.xaxis.set_major_formatter(formatter) + ax2.yaxis.set_major_formatter(formatter) + _cutoff = -int(time_samples_3 * 0.00058) + ax3.plot(x5_axis_displacement[_cutoff:], y5_axis_displacement[_cutoff:], label='Orbit') + ax3.plot(x5_axis_displacement[_cutoff:][0], y5_axis_displacement[_cutoff:][0], 'o', markersize=10, color='#636EFA') + ax3.plot(x6_axis_displacement[_cutoff:], y6_axis_displacement[_cutoff:], label='Orbit') + ax3.plot(x6_axis_displacement[_cutoff:][0], y6_axis_displacement[_cutoff:][0], 'o', markersize=10, color='#636EFA') + ax3.set_title('2596 RPM') + ax3.xaxis.set_major_formatter(formatter) + ax3.yaxis.set_major_formatter(formatter) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Major Axis Amplitude of the response at nodes 2 and 4 + """) + return + + +@app.cell +def _(Q_, np, rotor): + _n1 = 2 + m1_1 = 0.001 + _p1 = 0 + _speed = Q_(np.linspace(0, 4500, 950), 'RPM') + results_case1_1 = rotor.run_unbalance_response([_n1], [m1_1], [_p1], _speed) + _probe1 = (2, 'major') + _probe2 = (4, 'major') + fig_4 = results_case1_1.plot_magnitude(probe=[_probe1, _probe2], probe_units='degrees', frequency_units='RPM', amplitude_units='µm pkpk') + fig_4.update_layout(title='Major Axis Amplitude (µm pkpk)', yaxis=dict(title='Amplitude (µm pkpk)', type='log')) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### REFERENCES + [1] M. I. Friswell, J. E. T. Penny, S. D. Garvey, and A. W. Lees, Dynamics of Rotating Machines. Cambridge: Cambridge University Press, 2010. + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_27.rst b/docs/user_guide/example_27.rst new file mode 100644 index 000000000..ec1c9f8c6 --- /dev/null +++ b/docs/user_guide/example_27.rst @@ -0,0 +1,5 @@ +Example 27 +========== + +.. marimo:: example_27.py + :height: 700px diff --git a/docs/user_guide/example_28.py b/docs/user_guide/example_28.py new file mode 100644 index 000000000..d08d0c753 --- /dev/null +++ b/docs/user_guide/example_28.py @@ -0,0 +1,164 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 28 - Rigid rotor on isotropic bearings + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + This example is based on Example 6.2.1 page 239 from {cite}`friswell2010dynamics`. + + A uniform rigid rotor is shown in figure 3.8. The rotor has length of 0.5 m and a diameter of 0.2 m, and it is made from steel with a density of 7810 kg/m³. The rotor is supported at the ends by bearings. The horizontal and vertical stiffness are 1 MN/m at bearing 1 and 1.3 MN/m at bearing 2. The damping values in the horizontal and vertical supports are 10 Ns/m at bearing 1 and 13 Ns/m at bearing 2. This rotor is the same as that described in Example 3.5.3(b). Find the response to a mass eccentricity of 0.1 mm and plot the Campbell diagram. + """) + return + + +@app.cell +def _(): + import ross as rs + import plotly.graph_objects as go + import plotly.io as pio + import numpy as np + + Q_ = rs.Q_ + pio.renderers.default = "notebook" + return Q_, np, rs + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.2e9, rho=7810) + return (steel,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Creating rotor + """) + return + + +@app.cell +def _(rs, steel): + N = 6 + L = 0.5 / N + idl = 0 + odl = 0.2 + + shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for i in range(N)] + bearings = [ + rs.BearingElement(n=0, kxx=1e6, kyy=1.5e6, cxx=20, cyy=30), + rs.BearingElement(n=6, kxx=1.3e6, kyy=1.8e6, cxx=26, cyy=36), + ] + disks = [] + + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor.plot_rotor() + return odl, rotor + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Campbell Diagram + """) + return + + +@app.cell +def _(Q_, np, rotor): + fig = rotor.run_campbell(speed_range=Q_(list(range(0, 3500, 50)), "RPM"), frequencies=4) + fig.plot().show() + print( + "Wn [Hz] at 0 rpm =", + np.round((rotor.run_modal(speed=0 * np.pi / 30, num_modes=8).wn) / (2 * np.pi), 2), + ) + return + + +@app.cell +def _(rotor): + # Calculating equivalent unbalance mass + e = 0.1e-3 + shaft_radius = 0.1 + m_unb = (rotor.m * e) / shaft_radius + + print(f"Rotor mass = {round(rotor.m, 2)} kg") + print(f"Unbalance mass = {round(m_unb, 2)} kg") + return (m_unb,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Bode Plot + """) + return + + +@app.cell +def _(Q_, m_unb, np, odl, rotor): + n1 = 3 # position of the unbalance mass in the center of the rotor + m1 = m_unb * (odl / 2) + p1 = 0 # unbalance mass phase + _speed = Q_(np.linspace(0, 3500, 60), 'RPM') + results_case1 = rotor.run_unbalance_response([n1], [m1], [p1], _speed) + return (results_case1,) + + +@app.cell +def _(results_case1): + probe1 = (3, 0) + results_case1.plot( + probe=[probe1], + probe_units="degrees", + frequency_units="RPM", + amplitude_units="µm pkpk", + phase_units="degrees", + ) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Mode Shapes + """) + return + + +@app.cell +def _(np, rotor): + _speed = 3500 * np.pi / 30 + modal = rotor.run_modal(_speed) + for i in np.arange(0, 3.1, 1): + modal.plot_mode_3d(mode=int(i), frequency_units='RPM').show() + return (modal,) + + +@app.cell +def _(display, modal): + for mode in range(4): + display(modal.plot_orbit(mode, nodes=[0, 3, 6])) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_28.rst b/docs/user_guide/example_28.rst new file mode 100644 index 000000000..345105880 --- /dev/null +++ b/docs/user_guide/example_28.rst @@ -0,0 +1,5 @@ +Example 28 +========== + +.. marimo:: example_28.py + :height: 700px diff --git a/docs/user_guide/example_29.py b/docs/user_guide/example_29.py new file mode 100644 index 000000000..9f2efedc2 --- /dev/null +++ b/docs/user_guide/example_29.py @@ -0,0 +1,209 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 29 - Rigid rotor on anisotropic bearings + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + This example is based on Example 6.2.3 page 247 from {cite}`friswell2010dynamics`. + + A uniform rigid rotor is shown in figure 3.7. The rotor has length of 0.5 m and a diameter of 0.2 m, and it is made from steel with a density of 7810 kg/m³. The rotor is supported at the ends by bearings. The horizontal and vertical stiffnesses are 1 MN/m and 1.5 MN/m, respectively, at bearing 1 and 1.3 MN/m and 1.8 MN/m, respectively, at bearing 2. The damping values in the horizontal and vertical supports at both bearings are propostional to the stiffnesses and are 20 Ns/m and 30 Ns/m + at bearing 1 and 26 Ns/m and 36 Ns/mat bearing 2. This rotor is essentially the same as that described in Example 3.6.1. Determine the unbalance response over the speed range 0 to 4000 rev/min. + """) + return + + +@app.cell +def _(): + import ross as rs + import plotly.graph_objects as go + import plotly.io as pio + import numpy as np + + Q_ = rs.Q_ + pio.renderers.default = "notebook" + return Q_, np, rs + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.2e9, rho=7810) + return (steel,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Creating rotor + """) + return + + +@app.cell +def _(rs, steel): + N = 6 + L = 0.5 / N + idl = 0 + odl = 0.2 + + shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for i in range(N)] + bearings = [ + rs.BearingElement(n=0, kxx=1e6, kyy=1.5e6, cxx=20, cyy=30), + rs.BearingElement(n=6, kxx=1.3e6, kyy=1.8e6, cxx=26, cyy=36), + ] + disks = [] + + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor.plot_rotor() + return odl, rotor + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Campbell Diagram + """) + return + + +@app.cell +def _(Q_, np, rotor): + fig = rotor.run_campbell( + speed_range=Q_(list(range(0, 3500, 50)), "RPM"), frequencies=4 + ).plot() + fig.show() + print( + "Wn [Hz] at 0 rpm =", + np.round((rotor.run_modal(speed=0 * np.pi / 30, num_modes=8).wn) / (2 * np.pi), 2), + ) + return + + +@app.cell +def _(rotor): + # Calculating equivalent unbalance mass + e = 0.1e-3 + shaft_radius = 0.1 + m_unb = (rotor.m * e) / shaft_radius + + print(f"Rotor mass = {round(rotor.m, 2)} kg") + print(f"Unbalance mass = {round(m_unb, 2)} kg") + return (m_unb,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Bode Plot + """) + return + + +@app.cell +def _(Q_, m_unb, np, odl, rotor): + n1 = 3 # position of the unbalance mass in the center of the rotor + _m1 = m_unb * (odl / 2) + p1 = 0 # unbalance mass phase + _speed = Q_(np.linspace(0, 4000, 60), 'RPM') + results_case2 = rotor.run_unbalance_response([n1], [_m1], [p1], _speed) + return (results_case2,) + + +@app.cell +def _(results_case2): + # Response in x + probe_x = (0, 0) + results_case2.plot( + probe=[probe_x], + probe_units="degrees", + frequency_units="RPM", + amplitude_units="µm pkpk", + phase_units="degrees", + ) + return + + +@app.cell +def _(results_case2): + # response in y + probe_y = (0, 90) + results_case2.plot( + probe=[probe_y], + probe_units="degrees", + frequency_units="RPM", + amplitude_units="µm pkpk", + phase_units="degrees", + ) + return + + +@app.cell +def _(results_case2): + # Max response + probe_major = (0, "major") + results_case2.plot( + probe=[probe_major], + probe_units="degrees", + frequency_units="RPM", + amplitude_units="µm pkpk", + phase_units="degrees", + ) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Mode Shapes + """) + return + + +@app.cell +def _(np, rotor): + _speed = 4000 * np.pi / 30 + modal = rotor.run_modal(_speed) + for i in np.arange(0, 3.1, 1): + modal.plot_mode_3d(mode=int(i), frequency_units='RPM').show() + return (modal,) + + +@app.cell +def _(display, modal): + for mode in range(4): + display(modal.plot_orbit(mode, nodes=[0, 3, 6])) + return + + +@app.cell +def _(Q_, m_unb, np, odl, rotor): + n = [0, 1, 2, 3, 4, 5] # position of the unbalance mass along the rotor + _m1 = m_unb * (odl / 2) + m = [_m1 / 6, _m1 / 6, _m1 / 6, _m1 / 6, _m1 / 6, _m1 / 6] + p = [0, 0, 0, 0, 0, 0] # unbalance mass phase + _speed = Q_(np.linspace(0, 4000, 60), 'RPM') + results_case1 = rotor.run_unbalance_response(n, m, p, _speed) + probe1 = (0, 'major') + results_case1.plot(probe=[probe1], probe_units='degrees', frequency_units='RPM', amplitude_units='µm pkpk', phase_units='degrees') + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_29.rst b/docs/user_guide/example_29.rst new file mode 100644 index 000000000..fb7494a66 --- /dev/null +++ b/docs/user_guide/example_29.rst @@ -0,0 +1,5 @@ +Example 29 +========== + +.. marimo:: example_29.py + :height: 700px diff --git a/docs/user_guide/example_3.py b/docs/user_guide/example_3.py new file mode 100644 index 000000000..91ed558c6 --- /dev/null +++ b/docs/user_guide/example_3.py @@ -0,0 +1,119 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 3 - Isotropic Bearings, asymmetrical rotor. + =========== + + In this example, we use the rotor seen in Example 5.9.1 from {cite}`friswell2010dynamics`. + A 1.5-m-long shaft, with a diameter of $0.05 m$. The disks are keyed to the shaft at $0.5$ and $1 m$ from + one end. The left disk is $0.07 m$ thick with a diameter of $0.28 m$; the right disk + is $0.07 m$ thick with a diameter of $0.35 m$. For the shaft, $E = 211 GN/m^2$ and + $G = 81.2 GN/m^2$. There is no internal shaft damping. For both the shaft and the + disks, $\rho = 7,810 kg/m^3$. The shaft is supported by identical bearings at its ends. + + These bearings are isotropic and have a stiffness of $1 MN/m$ in both the x and + y directions. The bearings contribute no additional stiffness to the rotational + degrees of freedom and there is no damping or cross-coupling in the bearings. + """) + return + + +@app.cell +def _(): + import ross as rs + from ross.materials import steel + import numpy as np + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return np, rs, steel + + +@app.cell +def _(rs, steel): + # Classic Instantiation of the rotor + shaft_elements = [] + _bearing_seal_elements = [] + _disk_elements = [] + for i in range(6): + shaft_elements.append(rs.ShaftElement(L=0.25, material=steel, n=i, idl=0, odl=0.05)) + _disk_elements.append(rs.DiskElement.from_geometry(n=2, material=steel, width=0.07, i_d=0.05, o_d=0.28)) + _disk_elements.append(rs.DiskElement.from_geometry(n=4, material=steel, width=0.07, i_d=0.05, o_d=0.35)) + _bearing_seal_elements.append(rs.BearingElement(n=0, kxx=1000000.0, kyy=1000000.0, cxx=0, cyy=0)) + _bearing_seal_elements.append(rs.BearingElement(n=6, kxx=1000000.0, kyy=1000000.0, cxx=0, cyy=0)) + rotor591c = rs.Rotor(shaft_elements=shaft_elements, bearing_elements=_bearing_seal_elements, disk_elements=_disk_elements) + rotor591c.plot_rotor() + return (rotor591c,) + + +@app.cell +def _(rs, steel): + # From_section class method instantiation. + _bearing_seal_elements = [] + _disk_elements = [] + shaft_length_data = 3 * [0.5] + i_d = 3 * [0] + o_d = 3 * [0.05] + _disk_elements.append(rs.DiskElement.from_geometry(n=1, material=steel, width=0.07, i_d=0.05, o_d=0.28)) + _disk_elements.append(rs.DiskElement.from_geometry(n=2, material=steel, width=0.07, i_d=0.05, o_d=0.35)) + _bearing_seal_elements.append(rs.BearingElement(n=0, kxx=1000000.0, kyy=1000000.0, cxx=0, cyy=0)) + _bearing_seal_elements.append(rs.BearingElement(n=3, kxx=1000000.0, kyy=1000000.0, cxx=0, cyy=0)) + rotor591fs = rs.Rotor.from_section(brg_seal_data=_bearing_seal_elements, disk_data=_disk_elements, leng_data=shaft_length_data, idl_data=i_d, odl_data=o_d, material_data=steel) + rotor591fs.plot_rotor() + return (rotor591fs,) + + +@app.cell +def _(np, rotor591c, rotor591fs): + # Obtaining results (wn is in rad/s) + fig = rotor591c.run_campbell(np.linspace(0, 4000 * np.pi / 30, 50), frequencies=7).plot( + frequency_units="rad/s" + ) + fig.show() + + print("Normal Instantiation =", rotor591c.run_modal(speed=2000 * np.pi / 30).wn) + print("\n") + print("From Section Instantiation =", rotor591fs.run_modal(speed=2000 * np.pi / 30).wn) + return + + +@app.cell +def _(np, rotor591c): + # Obtaining modal results for w=4000RPM (wn is in rad/s) + speed = 4000 * np.pi / 30 + modal591c = rotor591c.run_modal(speed, num_modes=14) + + print("Normal Instantiation =", modal591c.wn) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_3.rst b/docs/user_guide/example_3.rst new file mode 100644 index 000000000..75a97e99e --- /dev/null +++ b/docs/user_guide/example_3.rst @@ -0,0 +1,5 @@ +Example 3 +========= + +.. marimo:: example_3.py + :height: 700px diff --git a/docs/user_guide/example_30.py b/docs/user_guide/example_30.py new file mode 100644 index 000000000..4aea2ef93 --- /dev/null +++ b/docs/user_guide/example_30.py @@ -0,0 +1,287 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 30 - Natural frequencies, mode shapes and natural frenquency map of a rigid rotor + ===== + This example is based on Example 3.6.1, Example 3.7.1 and Example 3.7.2 from {cite}`friswell2010dynamics`. + """) + return + + +@app.cell +def _(): + import ross as rs + from ross.units import Q_ + import plotly.io as pio + import numpy as np + import matplotlib.pyplot as plt + + # Set default plot renderer + pio.renderers.default = "notebook" + return Q_, np, plt, rs + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Creating shaft element + """) + return + + +@app.cell +def _(rs): + """ + Creates shaft elements with specified geometry and material properties. + """ + # Material Definition - Steel + steel = rs.Material(name="Steel", rho=7810, E=211e9, G_s=81.2e9) + + # Shaft Geometry Parameters + shaft_length = 0.5 # [m] + shaft_diameter = 0.2 # [m] + num_elements = 5 + element_length = shaft_length / num_elements + + # Create shaft elements + shaft_elements = [ + rs.ShaftElement( + L=element_length, + idl=0.0, # inner diameter (solid shaft) + odl=shaft_diameter, + material=steel, + shear_effects=True, + rotary_inertia=True, + gyroscopic=True, + ) + for _ in range(num_elements) + ] + return (shaft_elements,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Example 3.6.1 + + Determine the natural frequencies and the mode shapes of the rigid rotor of Example 3.5.1 for the following: + + (a) The horizontal and vertical support stiffnesses are 1.0 MN/m at bearing 1 and 1.3 MN/m at bearing 2 and the rotor spins at 4,000 rev/min. + + (b) The horizontal and vertical support stiffnesses are 1.0 and 1.1 MN/m, respectively, at bearing 1, and 1.3 and 1.4 MN/m, respectively, at bearing 2. Note that the bearings are anisotropic. Obtain the natural frequencies and mode shapes when the rotor is stationary and also rotating at 4,000 and 8,000 rev/min. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Rotor assembly + """) + return + + +@app.cell +def _(rs, shaft_elements): + """ + Creates rotor assemblies for each case. + """ + + # Bearing configuration for case (a) + bearings_a = [ + rs.BearingElement(n=0, kxx=1.0e6, kyy=1.0e6, cxx=0), # Bearing 1 + rs.BearingElement(n=5, kxx=1.3e6, kyy=1.3e6, cxx=0), # Bearing 2 + ] + + # Bearing configuration for case (b) + bearings_b = [ + rs.BearingElement(n=0, kxx=1.0e6, kyy=1.1e6, cxx=0), # Bearing 1 (anisotropic) + rs.BearingElement(n=5, kxx=1.3e6, kyy=1.4e6, cxx=0), # Bearing 2 (anisotropic) + ] + + # Create rotor assembly for case (a) + rotor_a = rs.Rotor(shaft_elements=shaft_elements, bearing_elements=bearings_a) + + # Create rotor assembly for case (b) + rotor_b = rs.Rotor(shaft_elements=shaft_elements, bearing_elements=bearings_b) + + # Plot rotor configuration for case (a), same as case (b) + rotor_a.plot_rotor().show() + return rotor_a, rotor_b + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Case (a) analyze + """) + return + + +@app.cell +def _(Q_, rotor_a): + """ + Analyzes case (a) from Example 3.6.1: + - Isotropic bearings (same stiffness in x and y directions) + - Rotor speed: 4000 RPM + """ + # Run modal analysis at 4000 rpm + modal_results_a = rotor_a.run_modal(speed=Q_(4000, 'RPM'), num_modes=8) + print('\nCase (a) Results - Isotropic Bearings at 4000 RPM:') + # Print natural frequencies + print(f"Natural Frequencies: {Q_(modal_results_a.wd, 'rad/s').to('Hz'):.2f}") + print('\nMode Shapes:') + for _mode in range(4): + # Plot mode shapes + modal_results_a.plot_mode_3d(mode=_mode, frequency_units='Hz', damping_parameter='damping_ratio').show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Case (b) analyze + """) + return + + +@app.cell +def _(Q_, rotor_b): + """ + Analyzes case (b) from Example 3.6.1: + - Anisotropic bearings (different stiffness in x and y directions) + - Multiple rotor speeds: 0, 4000, and 8000 RPM + """ + speeds = [0, 4000, 8000] + # Analyze at different speeds + modal_results = [] # RPM + for speed in speeds: + results = rotor_b.run_modal(speed=Q_(speed, 'RPM'), num_modes=8) + modal_results.append(results) + # Print natural frequencies and plot mode shapes for each speed + print(f'\nCase (b) Results - Anisotropic Bearings at {speed} RPM:') + print(f"Natural Frequencies: {Q_(results.wd, 'rad/s').to('Hz'):.2f}") + print(f'\nMode Shapes at {speed} RPM:') + for _mode in range(4): + results.plot_mode_3d(mode=_mode, frequency_units='Hz', damping_parameter='damping_ratio').show() # Print natural frequencies # Plot mode shapes + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Example 3.7.1 + + Plot the natural frequency maps for rotor spin speeds up to 20,000 rev/min for the rigid rotor described in Example 3.5.1, supported by the following bearing stiffnesses: + + (a) kx1 = 1.0 MN/m, ky1 = 1.0 MN/m, kx2 = 1.0 MN/m, ky2 = 1.0 MN/m + + (b) kx1 = 1.0 MN/m, ky1 = 1.0 MN/m, kx2 = 1.3 MN/m, ky2 = 1.3 MN/m + + (c) kx1 = 1.0 MN/m, ky1 = 1.5 MN/m, kx2 = 1.0 MN/m, ky2 = 1.5 MN/m + + (d) kx1 = 1.0 MN/m, ky1 = 1.5 MN/m, kx2 = 1.3 MN/m, ky2 = 2.0 MN/m + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Rotor assembly and campbell plot + """) + return + + +@app.cell +def _(Q_, np, rs, shaft_elements): + """ + Creates rotor assemblies for each bearing case and plot campbell diagram. + """ + bearing_configs = {'(a)': [(1000000.0, 1000000.0), (1000000.0, 1000000.0)], '(b)': [(1000000.0, 1000000.0), (1300000.0, 1300000.0)], '(c)': [(1000000.0, 1500000.0), (1000000.0, 1500000.0)], '(d)': [(1000000.0, 1500000.0), (1300000.0, 2000000.0)]} + speed_range = np.linspace(0, Q_(20000, 'RPM'), 100) + for (case, ((_kxx1, _kyy1), (kxx2, kyy2))) in bearing_configs.items(): + _bearing1 = rs.BearingElement(n=0, kxx=_kxx1, kyy=_kyy1, cxx=0) + bearing2 = rs.BearingElement(n=5, kxx=kxx2, kyy=kyy2, cxx=0) + _rotor = rs.Rotor(shaft_elements=shaft_elements, bearing_elements=[_bearing1, bearing2]) + campbell = _rotor.run_campbell(speed_range=speed_range, frequencies=4) + # Speed range (rad/s) + # Run Campbell diagram for each case + campbell.plot(title=f'Campbell Diagram - Case {case}', frequency_units='Hz').show() # Rotor Assembly # Campbell diagram plot + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Example 3.7.2 + + Determine the effect of varying the stiffness of bearing 2 for the rigid rotor as described in Example 3.5.1 for a rotor spin speed of 5,000 rev/min. Assume the bearing is isotropic and vary kx2 = ky2 in the range 0.4 to 2.0 MN/m. + + The properties of bearing 1 are: + + (a) kx1 = 1.0 MN/m, ky1 = 1.0 MN/m + + (b) kx1 = 1.0 MN/m, ky1 = 2.0 MN/m + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Rotor assembly and natural frequency maps + """) + return + + +@app.cell +def _(Q_, np, plt, rs, shaft_elements): + """ + Creates rotor assemblies for each bearing case and plot natural frequency maps. + """ + rotor_speed = Q_(5000, 'RPM') + # Rotor speed (rpm to rad/s) + k2_values = np.linspace(400000.0, 2000000.0, 20) + bearing1_cases = {'(a)': (1000000.0, 1000000.0), '(b)': (1000000.0, 2000000.0)} + # Stiffness range for Bearing 2 (MN/m -> N/m) + for (label, (_kxx1, _kyy1)) in bearing1_cases.items(): + modes = [] + # Bearing 1 configurations + for k in k2_values: + bearing0 = rs.BearingElement(n=0, kxx=_kxx1, kyy=_kyy1, cxx=0) + _bearing1 = rs.BearingElement(n=5, kxx=k, kyy=k, cxx=0) + _rotor = rs.Rotor(shaft_elements=shaft_elements, bearing_elements=[bearing0, _bearing1]) + modal = _rotor.run_modal(speed=rotor_speed) + # Loop over bearing1 cases + modes.append(modal.wn[:4]) + plt.figure(figsize=(10, 6)) + modes = np.array(modes) + for i in range(4): + plt.plot(k2_values / 1000000.0, modes[:, i] / (2 * np.pi), label=f'Modo {i + 1} {label}') # Isotropic + plt.xlabel('Bearing 2 Stiffness (MN/m)') + plt.ylabel('Natural Frequency (Hz)') + plt.title(f'Effect of Varying Bearing 2 Stiffness at 5000 rpm in Case {label}') + plt.legend() + plt.xlim(0.4, 2) + plt.ylim(0, 60) + plt.tight_layout() # First two natural frequencies + plt.show() # Plot setup # Plot adjustments + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_30.rst b/docs/user_guide/example_30.rst new file mode 100644 index 000000000..00af7cd34 --- /dev/null +++ b/docs/user_guide/example_30.rst @@ -0,0 +1,5 @@ +Example 30 +========== + +.. marimo:: example_30.py + :height: 700px diff --git a/docs/user_guide/example_31.py b/docs/user_guide/example_31.py new file mode 100644 index 000000000..739e28238 --- /dev/null +++ b/docs/user_guide/example_31.py @@ -0,0 +1,153 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 31 - Isotropic Bearings with Damping + + This example is based on Example 5.9.5 from {cite}friswell2010dynamics + + The isotropic bearing Example 5.9.1 (Example 17) is repeated but with damping in the bearings. The, _x_ and _y_ directions are uncoupled, with a translational stiffness of 1 MN/m and a damping of 3 kNs/m in each direction. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import pandas as pd + + from ross.units import Q_ + + return Q_, np, pd, rs + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.2e9, rho=7810) + return (steel,) + + +@app.cell +def _(rs, steel): + L = 0.25 + N = 6 + idl = 0 + odl = 0.05 + + shaft = [rs.ShaftElement(L=L, idl=idl, odl=odl, material=steel) for i in range(N)] + + bearings = [ + rs.BearingElement(n=0, kxx=1e6, kyy=1e6, cxx=3e3, cyy=3e3, scale_factor=2), + rs.BearingElement(n=len(shaft), kxx=1e6, kyy=1e6, cxx=3e3, cyy=3e3, scale_factor=2), + ] + + disks = [ + rs.DiskElement.from_geometry( + n=2, material=steel, width=0.07, i_d=odl, o_d=0.28, scale_factor="mass" + ), + rs.DiskElement.from_geometry( + n=4, material=steel, width=0.07, i_d=odl, o_d=0.35, scale_factor="mass" + ), + ] + + rotor = rs.Rotor(shaft_elements=shaft, disk_elements=disks, bearing_elements=bearings) + rotor.plot_rotor() + return (rotor,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Plotting the Campbell Diagram + """) + return + + +@app.cell +def _(Q_, rotor): + campbell = rotor.run_campbell(speed_range=Q_(list(range(0, 4500, 50)), "RPM")) + return (campbell,) + + +@app.cell +def _(campbell): + campbell.plot(frequency_units="RPM", frequencies=8) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Plotting the Mode Shapes and Damped Natural Frequencies + """) + return + + +@app.cell +def _(Q_, rotor): + modal = rotor.run_modal(speed=Q_(4000, "RPM")) + return (modal,) + + +@app.cell +def _(display, modal): + for mode in range(7): + display(modal.plot_mode_3d(mode, frequency_units="Hz")) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Creating table with eigenvalues, natural frequencies and damping ratios + """) + return + + +@app.cell +def _(Q_, display, np, pd, rotor): + def modal_table(modal_results, rpm): + eig = modal_results.evalues + omega_n = np.abs(eig) + omega_d = np.imag(eig) + zeta = -np.real(eig) / omega_n + fn = omega_n / (2 * np.pi) + fd = omega_d / (2 * np.pi) + + return pd.DataFrame( + { + "Speed (RPM)": [rpm] * 8, + "Root s (rad/s)": eig[:8], + "Wn (Hz)": fn[:8], + "Wd (Hz)": fd[:8], + "Damping Ratio": zeta[:8], + } + ) + + + modal_0 = rotor.run_modal(speed=Q_(0, "RPM")) + modal_4000 = rotor.run_modal(speed=Q_(4000, "RPM")) + + df_modal_0 = modal_table(modal_0, rpm=0) + df_modal_4000 = modal_table(modal_4000, rpm=4000) + df_combined = pd.concat([df_modal_0, df_modal_4000], ignore_index=True) + + display(df_combined) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_31.rst b/docs/user_guide/example_31.rst new file mode 100644 index 000000000..b8b419c86 --- /dev/null +++ b/docs/user_guide/example_31.rst @@ -0,0 +1,5 @@ +Example 31 +========== + +.. marimo:: example_31.py + :height: 700px diff --git a/docs/user_guide/example_32.py b/docs/user_guide/example_32.py new file mode 100644 index 000000000..7914997c2 --- /dev/null +++ b/docs/user_guide/example_32.py @@ -0,0 +1,238 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Example 32 - Response to Forces Applied through Auxiliary Bearings + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + This example is based on Example 6.3.3 page 264 from {cite}`friswell2010dynamics`. + + The rotor-bearing system described in Example 6.3.1 (and shown in Figure 6.18 - Page 253) rotates at 3,000 rev/min. Each bearing has a stiffness of 1 MN/m and a damping of 100 Ns/m in both the x and y directions. For test purposes, the rotor is excited via an auxiliary bearing that is attached to the rotor at midspan. Determine the response of the disk at node 3 to forces acting on an auxiliary bearing. The system is modeled using six elements, giving 28 degrees of freedom. The forces at the auxiliary bearing are (a) a rotating out-of-balance of 0.0001 kg.m, and (b) a harmonic force of 10 N. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + from ross import Probe + from ross.units import Q_ + + import plotly.graph_objects as go + + return Probe, Q_, go, np, rs + + +@app.cell +def _(rs): + steel = rs.Material("steel", E=211e9, G_s=81.1e9, rho=7810) + return (steel,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Creating Rotor + """) + return + + +@app.cell +def _(rs, steel): + # Shaft parameters + shaft_length = 1.5 # meters + shaft_diameter = 0.05 # meters + n_elements = 6 + L_elem = shaft_length / n_elements + + shaft_elements = [ + rs.ShaftElement( + L=L_elem, + idl=0.0, + odl=shaft_diameter, + material=steel, + shear_effects=True, + rotary_inertia=True, + gyroscopic=True, + ) + for _ in range(n_elements) + ] + + # Disk parameters + disk1 = rs.DiskElement.from_geometry( + n=2, + material=steel, + width=0.070, + i_d=shaft_diameter, + o_d=0.280, + ) + disk2 = rs.DiskElement.from_geometry( + n=4, + material=steel, + width=0.070, + i_d=shaft_diameter, + o_d=0.350, + ) + + + # Bearings at ends (nodes 0 and 6) + kxx = 1e6 # N/m + cxx = 100 # Ns/m + bearing1 = rs.BearingElement( + n=0, + kxx=kxx, + kyy=kxx, + cxx=cxx, + ) + bearing2 = rs.BearingElement( + n=6, + kxx=kxx, + kyy=kxx, + cxx=cxx, + ) + + # Assemble rotor + rotor = rs.Rotor( + shaft_elements=shaft_elements, + disk_elements=[disk1, disk2], + bearing_elements=[bearing1, bearing2], + ) + rotor.plot_rotor() + return (rotor,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Response for a rotating out-of-balance + """) + return + + +@app.cell +def _(Probe, Q_, np, rotor): + # Unbalance excitation at midspan (node 3) + unbalance_response = rotor.run_unbalance_response( + node=3, + unbalance_magnitude=0.0001, + unbalance_phase=0, + frequency=Q_(np.linspace(0, 4000, 1000), "RPM"), + ) + + fig2 = unbalance_response.plot_magnitude( + probe=[Probe(2, Q_(90, "deg"))], yaxis=dict(type="log"), frequency_units="RPM" + ) + fig2.update_layout(yaxis_range=[-8, -2]) + fig2.show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Response due to vertical a harmonic excitation at the auxiliary bearing + """) + return + + +@app.cell +def _(Q_, go, np, rotor): + F0 = 10 + node = 3 + probe = 2 + + speed_range = Q_([100, 500, 1000, 2000, 3000], "RPM") + frequency_range = Q_(np.linspace(0, 65, 1000), "Hz").to_base_units().m + t = np.arange(0, 10, 0.001) + + num_dof = rotor.number_dof + + fig = go.Figure() + + for speed in speed_range: + probe_resp = [] + + for w in frequency_range: + # Create vertical harmonic force + F = np.zeros((len(t), rotor.ndof)) + + dofy = num_dof * node + 1 + F[:, dofy] += F0 * np.sin(w * t) + + # Run time response + time_resp = rotor.run_time_response(speed, F, t) + + # Extract response for the probe + response = time_resp.yout + + init_step = int(2 * len(t) / 3) + + dofx = num_dof * probe + dofy = num_dof * probe + 1 + + x = response[init_step:, dofx] * 0 + y = response[init_step:, dofy] + + lateral_max = max(max(abs(x)), max(abs(y))) + probe_resp.append(lateral_max) + + # Adding the response to figure + fig.add_trace( + go.Scatter( + x=Q_(frequency_range, "rad/s").to("Hz").m, + y=np.array(probe_resp), + mode="lines", + name=f"{speed.to('RPM').m} RPM", + line=dict(width=2), + ) + ) + return (fig,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Plotting response + """) + return + + +@app.cell +def _(fig): + fig.update_layout( + xaxis=dict(title="Frequency (Hz)"), + yaxis=dict( + title="Amplitude (m)", + type="log", + range=[-8, -2], + exponentformat="power", + ), + legend=dict( + title=dict(text="Rotor Speed"), + ), + ) + + fig.show() + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_32.rst b/docs/user_guide/example_32.rst new file mode 100644 index 000000000..9be840e76 --- /dev/null +++ b/docs/user_guide/example_32.rst @@ -0,0 +1,5 @@ +Example 32 +========== + +.. marimo:: example_32.py + :height: 700px diff --git a/docs/user_guide/example_4.py b/docs/user_guide/example_4.py new file mode 100644 index 000000000..89bb1af0a --- /dev/null +++ b/docs/user_guide/example_4.py @@ -0,0 +1,143 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 4 - Anisotropic Bearings. + ==== + In this example, we use the rotor seen in Example 5.9.2 from {cite}`friswell2010dynamics`. + + Both bearings have a stiffness of 1 MN/m in the x direction and 0.8 MN/m in the + y direction. Calculate the eigenvalues and mode shapes at 0 and 4,000 rev/min + and plot the natural frequency map for rotational speeds up to 4,500 rev/min. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return np, rs + + +@app.cell +def _(rs): + # Classic Instantiation of the rotor + shaft_elements = [] + _bearing_seal_elements = [] + _disk_elements = [] + steel = rs.steel + for _i in range(6): + shaft_elements.append(rs.ShaftElement(L=0.25, material=steel, n=_i, idl=0, odl=0.05)) + _disk_elements.append(rs.DiskElement.from_geometry(n=2, material=steel, width=0.07, i_d=0.05, o_d=0.28)) + _disk_elements.append(rs.DiskElement.from_geometry(n=4, material=steel, width=0.07, i_d=0.05, o_d=0.35)) + _bearing_seal_elements.append(rs.BearingElement(n=0, kxx=1000000.0, kyy=800000.0, cxx=0, cyy=0)) + _bearing_seal_elements.append(rs.BearingElement(n=6, kxx=1000000.0, kyy=800000.0, cxx=0, cyy=0)) + rotor592c = rs.Rotor(shaft_elements=shaft_elements, bearing_elements=_bearing_seal_elements, disk_elements=_disk_elements) + rotor592c.plot_rotor() + return rotor592c, steel + + +@app.cell +def _(rs, steel): + # From_section class method instantiation. + _bearing_seal_elements = [] + _disk_elements = [] + shaft_length_data = 3 * [0.5] + i_d = 3 * [0] + o_d = 3 * [0.05] + _disk_elements.append(rs.DiskElement.from_geometry(n=1, material=steel, width=0.07, i_d=0.05, o_d=0.28)) + _disk_elements.append(rs.DiskElement.from_geometry(n=2, material=steel, width=0.07, i_d=0.05, o_d=0.35)) + _bearing_seal_elements.append(rs.BearingElement(n=0, kxx=1000000.0, kyy=800000.0, cxx=0, cyy=0)) + _bearing_seal_elements.append(rs.BearingElement(n=3, kxx=1000000.0, kyy=800000.0, cxx=0, cyy=0)) + rotor592fs = rs.Rotor.from_section(brg_seal_data=_bearing_seal_elements, disk_data=_disk_elements, leng_data=shaft_length_data, idl_data=i_d, odl_data=o_d, material_data=steel) + rotor592fs.plot_rotor() + return (rotor592fs,) + + +@app.cell +def _(rotor592c, rotor592fs): + # Obtaining results for speed = 150 rad/s (wn is in rad/s) + + modal592c = rotor592c.run_modal(150.0) + modal592fs = rotor592fs.run_modal(150.0) + + print("Normal Instantiation =", modal592c.wn) + print("\n") + print("From Section Instantiation =", modal592fs.wn) + return + + +@app.cell +def _(np, rotor592c): + # Obtaining results for w=4000RPM (wn is in rad/s) + speed = 4000 * np.pi / 30 + modal592c_1 = rotor592c.run_modal(speed, num_modes=14) + print('Normal Instantiation =', modal592c_1.wn) + return (modal592c_1,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + - Campbell Diagram + """) + return + + +@app.cell +def _(np, rotor592c): + campbell = rotor592c.run_campbell(np.linspace(0, 4000 * np.pi / 30, 50), frequencies=7) + # Plotting frequency in RPM + fig = campbell.plot(frequency_units="RPM") + fig.show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + - Mode Shapes + """) + return + + +@app.cell +def _(modal592c_1): + for _i in range(7): + modal592c_1.plot_mode_3d(mode=int(_i)).show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_4.rst b/docs/user_guide/example_4.rst new file mode 100644 index 000000000..7431070a5 --- /dev/null +++ b/docs/user_guide/example_4.rst @@ -0,0 +1,5 @@ +Example 4 +========= + +.. marimo:: example_4.py + :height: 700px diff --git a/docs/user_guide/example_5.py b/docs/user_guide/example_5.py new file mode 100644 index 000000000..744fdd2d0 --- /dev/null +++ b/docs/user_guide/example_5.py @@ -0,0 +1,107 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 5 - Cross-coupled bearings. + ===== + In this example, we use the rotor seen in Example 5.9.4 from {cite}`friswell2010dynamics`. + + This system is the same as that of + Example 3, except that some coupling is introduced in the bearings between the x and y directions. The bearings have direct stiffnesses of $1 MN/m$ and cross-coupling stiffnesses of $0.5 MN/m$. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return np, rs + + +@app.cell +def _(rs): + # Classic Instantiation of the rotor + shaft_elements = [] + _bearing_seal_elements = [] + _disk_elements = [] + Steel = rs.steel + for i in range(6): + shaft_elements.append(rs.ShaftElement(L=0.25, material=Steel, n=i, idl=0, odl=0.05)) + _disk_elements.append(rs.DiskElement.from_geometry(n=2, material=Steel, width=0.07, i_d=0.05, o_d=0.28)) + _disk_elements.append(rs.DiskElement.from_geometry(n=4, material=Steel, width=0.07, i_d=0.05, o_d=0.35)) + _bearing_seal_elements.append(rs.BearingElement(n=0, kxx=1000000.0, kyy=1000000.0, kxy=500000.0, cxx=0, cyy=0)) + _bearing_seal_elements.append(rs.BearingElement(n=6, kxx=1000000.0, kyy=1000000.0, kxy=500000.0, cxx=0, cyy=0)) + rotor594c = rs.Rotor(shaft_elements=shaft_elements, bearing_elements=_bearing_seal_elements, disk_elements=_disk_elements) + rotor594c.plot_rotor() + return Steel, rotor594c + + +@app.cell +def _(Steel, rs): + # From_section class method instantiation. + _bearing_seal_elements = [] + _disk_elements = [] + shaft_length_data = 3 * [0.5] + i_d = 3 * [0] + o_d = 3 * [0.05] + _disk_elements.append(rs.DiskElement.from_geometry(n=1, material=Steel, width=0.07, i_d=0.05, o_d=0.28)) + _disk_elements.append(rs.DiskElement.from_geometry(n=2, material=Steel, width=0.07, i_d=0.05, o_d=0.35)) + _bearing_seal_elements.append(rs.BearingElement(n=0, kxx=1000000.0, kyy=1000000.0, cxx=0, cyy=0)) + _bearing_seal_elements.append(rs.BearingElement(n=3, kxx=1000000.0, kyy=1000000.0, cxx=0, cyy=0)) + rotor594fs = rs.Rotor.from_section(brg_seal_data=_bearing_seal_elements, disk_data=_disk_elements, leng_data=shaft_length_data, idl_data=i_d, odl_data=o_d, material_data=Steel) + rotor594fs.plot_rotor() + return (rotor594fs,) + + +@app.cell +def _(rotor594c, rotor594fs): + # Obtaining results for w=0 (wn is in rad/s) + _modal594c = rotor594c.run_modal(0) + modal594fs = rotor594fs.run_modal(0) + print('Normal Instantiation =', _modal594c.wn) + print('\n') + print('From Section Instantiation =', modal594fs.wn) + return + + +@app.cell +def _(np, rotor594c): + # Obtaining results for w=4000RPM (wn is in rad/s) + _modal594c = rotor594c.run_modal(4000 * np.pi / 30) + print('Normal Instantiation =', _modal594c.wn) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_5.rst b/docs/user_guide/example_5.rst new file mode 100644 index 000000000..acc2c1b29 --- /dev/null +++ b/docs/user_guide/example_5.rst @@ -0,0 +1,5 @@ +Example 5 +========= + +.. marimo:: example_5.py + :height: 700px diff --git a/docs/user_guide/example_6.py b/docs/user_guide/example_6.py new file mode 100644 index 000000000..b87692875 --- /dev/null +++ b/docs/user_guide/example_6.py @@ -0,0 +1,117 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 6 - Isotropic bearings with damping. + ===== + In this example, we use the rotor seen in Example 5.9.5 from {cite}`friswell2010dynamics`. + + The isotropic bearing Example 3 is repeated but with damping in the bearings. The, x and y directions are + uncoupled, with a translational stiffness of 1 MN/m and a damping of 3 kNs/m + in each direction. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return np, rs + + +@app.cell +def _(rs): + # Classic Instantiation of the rotor + shaft_elements = [] + _bearing_seal_elements = [] + _disk_elements = [] + steel = rs.steel + for i in range(6): + shaft_elements.append(rs.ShaftElement(L=0.25, material=steel, n=i, idl=0, odl=0.05)) + _disk_elements.append(rs.DiskElement.from_geometry(n=2, material=steel, width=0.07, i_d=0.05, o_d=0.28)) + _disk_elements.append(rs.DiskElement.from_geometry(n=4, material=steel, width=0.07, i_d=0.05, o_d=0.35)) + _bearing_seal_elements.append(rs.BearingElement(n=0, kxx=1000000.0, kyy=1000000.0, cxx=3000.0, cyy=3000.0)) + _bearing_seal_elements.append(rs.BearingElement(n=6, kxx=1000000.0, kyy=1000000.0, cxx=3000.0, cyy=3000.0)) + rotor595c = rs.Rotor(shaft_elements=shaft_elements, bearing_elements=_bearing_seal_elements, disk_elements=_disk_elements) + rotor595c.plot_rotor() + return rotor595c, steel + + +@app.cell +def _(rs, steel): + # From_section class method instantiation. + _bearing_seal_elements = [] + _disk_elements = [] + shaft_length_data = 3 * [0.5] + i_d = 3 * [0] + o_d = 3 * [0.05] + _disk_elements.append(rs.DiskElement.from_geometry(n=1, material=steel, width=0.07, i_d=0.05, o_d=0.28)) + _disk_elements.append(rs.DiskElement.from_geometry(n=2, material=steel, width=0.07, i_d=0.05, o_d=0.35)) + _bearing_seal_elements.append(rs.BearingElement(n=0, kxx=1000000.0, kyy=1000000.0, cxx=3000.0, cyy=3000.0)) + _bearing_seal_elements.append(rs.BearingElement(n=3, kxx=1000000.0, kyy=1000000.0, cxx=3000.0, cyy=3000.0)) + rotor595fs = rs.Rotor.from_section(brg_seal_data=_bearing_seal_elements, disk_data=_disk_elements, leng_data=shaft_length_data, idl_data=i_d, odl_data=o_d, material_data=steel) + rotor595fs.plot_rotor() + return (rotor595fs,) + + +@app.cell +def _(np, rotor595c, rotor595fs): + # Obtaining results for w=0 + _modal595c = rotor595c.run_modal(0) + modal595fs = rotor595fs.run_modal(0) + print('Normal Instantiation =', _modal595c.wn * 60 / (2 * np.pi), '[RPM]') + print('\n') + print('From Section Instantiation =', modal595fs.wn * 60 / (2 * np.pi), '[RPM]') + return + + +@app.cell +def _(np, rotor595c): + # Obtaining results for w=4000RPM + _modal595c = rotor595c.run_modal(4000 * np.pi / 30, num_modes=14) + print('Normal Instantiation =', _modal595c.wn * 60 / (2 * np.pi), '[RPM]') # speed input in rad/s + return + + +@app.cell +def _(np, rotor595c): + # The input units must be according to your unit standard system + campbell = rotor595c.run_campbell(np.linspace(0, 4000 * np.pi / 30, 50), frequencies=7) + # Plotting frequency in RPM + campbell.plot(frequency_units="RPM") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_6.rst b/docs/user_guide/example_6.rst new file mode 100644 index 000000000..0b6795c66 --- /dev/null +++ b/docs/user_guide/example_6.rst @@ -0,0 +1,5 @@ +Example 6 +========= + +.. marimo:: example_6.py + :height: 700px diff --git a/docs/user_guide/example_7.py b/docs/user_guide/example_7.py new file mode 100644 index 000000000..ad3c03561 --- /dev/null +++ b/docs/user_guide/example_7.py @@ -0,0 +1,117 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 7 - Hydrodynamic Bearings + ===== + In this example, we use the rotor seen in Example 5.9.6 from {cite}`friswell2010dynamics`. + + Same rotor of Example 3, but the bearings are replaced with hydrodynamic bearings. In order to instantiate them, rather than giving the stiffness and damping data, we will calculate them using their hydrodynamic data, as provided by Example 5.5.1 from the book: The oil-film bearings have a diameter of 100 mm, are 30 mm long, and each supports a static load of 525 N, which represents half of the weight of the rotor. The radial clearance in the bearings is 0.1 mm and the oil film has a viscosity of 0.1 Pa s. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + + Q_ = rs.Q_ + return Q_, np, rs + + +@app.cell +def _(np, rs): + # Classic Instantiation of the rotor + shaft_elements = [] + disk_elements = [] + steel = rs.materials.steel + for i in range(6): + shaft_elements.append(rs.ShaftElement(L=0.25, material=steel, n=i, idl=0, odl=0.05)) + disk_elements.append(rs.DiskElement.from_geometry(n=2, material=steel, width=0.07, i_d=0.05, o_d=0.28)) + disk_elements.append(rs.DiskElement.from_geometry(n=4, material=steel, width=0.07, i_d=0.05, o_d=0.35)) + bearing = rs.BearingFluidFlow(n=0, nz=30, ntheta=20, length=0.03, omega=np.linspace(1, 5000, 5), p_in=0, p_out=0, radius_rotor=0.0499, radius_stator=0.05, visc=0.1, rho=860.0, load=525) + _bearing_copy = rs.BearingElement(n=6, frequency=bearing.frequency, kxx=bearing.kxx, kxy=bearing.kxy, kyx=bearing.kyx, kyy=bearing.kyy, cxx=bearing.cxx, cxy=bearing.cxy, cyx=bearing.cyx, cyy=bearing.cyy) + _bearing_seal_elements = [bearing, _bearing_copy] + rotor = rs.Rotor(shaft_elements=shaft_elements, bearing_elements=_bearing_seal_elements, disk_elements=disk_elements) + # copy bearing to decrease compute time and set node + rotor.plot_rotor() + return bearing, disk_elements, shaft_elements + + +@app.cell +def _(bearing, disk_elements, rs, shaft_elements): + _bearing_copy = rs.BearingElement(n=6, frequency=bearing.frequency, kxx=bearing.kxx, kxy=bearing.kxy, kyx=bearing.kyx, kyy=bearing.kyy, cxx=bearing.cxx, cxy=bearing.cxy, cyx=bearing.cyx, cyy=bearing.cyy) + _bearing_seal_elements = [bearing, _bearing_copy] + rotor_1 = rs.Rotor(shaft_elements=shaft_elements, bearing_elements=_bearing_seal_elements, disk_elements=disk_elements) + rotor_1.plot_rotor() + return (rotor_1,) + + +@app.cell +def _(Q_, rotor_1): + _campbell = rotor_1.run_campbell(speed_range=Q_(list(range(0, 4500, 50)), 'RPM')) + return + + +@app.cell +def _(np, rotor_1): + # Obtaining results for w=4000RPM + modal = rotor_1.run_modal(4000 * np.pi / 30) + print('Normal Instantiation =', modal.wn / (2 * np.pi)) + return (modal,) + + +@app.cell +def _(np, rotor_1): + _campbell = rotor_1.run_campbell(np.linspace(0, 4000 * np.pi / 30, 50)) + _campbell.plot(frequency_units='RPM') + return + + +@app.cell +def _(display, modal): + for _mode in range(6): + display(modal.plot_mode_3d(_mode, frequency_units='Hz')) + return + + +@app.cell +def _(display, modal): + for _mode in range(6): + display(modal.plot_orbit(_mode, nodes=[2, 4])) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_7.rst b/docs/user_guide/example_7.rst new file mode 100644 index 000000000..f28d9360e --- /dev/null +++ b/docs/user_guide/example_7.rst @@ -0,0 +1,5 @@ +Example 7 +========= + +.. marimo:: example_7.py + :height: 700px diff --git a/docs/user_guide/example_8.py b/docs/user_guide/example_8.py new file mode 100644 index 000000000..0b263b240 --- /dev/null +++ b/docs/user_guide/example_8.py @@ -0,0 +1,104 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 8 - Overhung rotor. + ========= + In this example, we use the rotor seen in Example 5.9.9 from {cite}`friswell2010dynamics`. + + The shaft is $1.5m$ long and the diameter is $50 mm$ with a disk of diameter $350mm$ and thickness $70 mm$. The two bearings, have a stiffness of $10 MN/m$ in each direction. The shaft and disk are made of steel. Damping is neglected. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return np, rs + + +@app.cell +def _(rs): + shaft_elements = [] + disk_elements = [] + bearing_seal_elements = [] + steel = rs.steel + + bearing_seal_elements.append(rs.BearingElement(n=0, kxx=10e6, kyy=10e6, cxx=0, cyy=0)) + bearing_seal_elements.append(rs.BearingElement(n=1, kxx=10e6, kyy=10e6, cxx=0, cyy=0)) + + shaft_elements.append(rs.ShaftElement(material=steel, n=0, L=1, odl=0.05, idl=0)) + shaft_elements.append(rs.ShaftElement(material=steel, n=1, L=0.5, odl=0.05, idl=0)) + + disk_elements.append( + rs.DiskElement.from_geometry(n=2, i_d=0.05, o_d=0.35, width=0.07, material=steel) + ) + + # Moment approach + overhung_rotor = rs.Rotor( + shaft_elements=shaft_elements, + bearing_elements=bearing_seal_elements, + disk_elements=disk_elements, + ) + # from section approach + leng_data = [1.0, 0.5] + + overhung_from_section_rotor = rs.Rotor.from_section( + brg_seal_data=bearing_seal_elements, + disk_data=disk_elements, + leng_data=leng_data, + idl_data=[0, 0], + odl_data=[0.05, 0.05], + material_data=steel, + ) + overhung_from_section_rotor.plot_rotor() + return (overhung_from_section_rotor,) + + +@app.cell +def _(np, overhung_from_section_rotor): + modal = overhung_from_section_rotor.run_modal(0) + + print("From section approach =", modal.wn / (2 * np.pi)) + return + + +@app.cell +def _(np, overhung_from_section_rotor): + overhung_from_section_rotor.run_campbell(np.linspace(0, 4000 * np.pi / 30, 50)).plot() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_8.rst b/docs/user_guide/example_8.rst new file mode 100644 index 000000000..d5a92438b --- /dev/null +++ b/docs/user_guide/example_8.rst @@ -0,0 +1,5 @@ +Example 8 +========= + +.. marimo:: example_8.py + :height: 700px diff --git a/docs/user_guide/example_9.py b/docs/user_guide/example_9.py new file mode 100644 index 000000000..6f22d97a9 --- /dev/null +++ b/docs/user_guide/example_9.py @@ -0,0 +1,178 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Example 9 - Hydrodynamic Journal Bearings (using Fluid Flow methods) + ===== + In this example, we use the hydrodynamic bearing seen in Example 5.5.1 from {cite}`friswell2010dynamics`. + + It is the same bearing of Example 7, only this time we stick to the methods provided by the Fluid Flow subpackage of ROSS. We instantiate a Pressure Matrix object with the data given by the Example 5.5.1 from the book: The oil-film bearing has a diameter of 100 mm, is 30 mm long, and supports a static load of 525 N. The radial clearance is 0.1 mm and the oil film has a viscosity of 0.1 Pa s. When instantiated, a Pressure Matrix must be given either the eccentricity, or load of the bearing, or both. The one not parameter not given is them calculated based on the other one. + """) + return + + +@app.cell +def _(): + from ross.bearings import fluid_flow as flow + from ross.bearings.fluid_flow_geometry import ( + sommerfeld_number, + modified_sommerfeld_number, + ) + from ross.bearings.fluid_flow_graphics import plot_eccentricity, plot_pressure_theta + from ross.bearings.fluid_flow_coefficients import ( + calculate_stiffness_and_damping_coefficients, + ) + + import numpy as np + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return ( + calculate_stiffness_and_damping_coefficients, + flow, + modified_sommerfeld_number, + plot_eccentricity, + plot_pressure_theta, + sommerfeld_number, + ) + + +@app.cell +def _(flow): + # Instantiating a Pressure Matrix + nz = 8 + ntheta = 128 + length = 0.03 + omega = 157.1 + p_in = 0.0 + p_out = 0.0 + radius_rotor = 0.0499 + radius_stator = 0.05 + load = 525 + visc = 0.1 + rho = 860.0 + my_fluid_flow = flow.FluidFlow( + nz, + ntheta, + length, + omega, + p_in, + p_out, + radius_rotor, + radius_stator, + visc, + rho, + load=load, + ) + return my_fluid_flow, nz + + +@app.cell +def _(my_fluid_flow): + # Getting the eccentricity + + my_fluid_flow.eccentricity + return + + +@app.cell +def _(modified_sommerfeld_number, my_fluid_flow, sommerfeld_number): + # Calculating the modified sommerfeld number and the sommerfeld number + + modified_s = modified_sommerfeld_number( + my_fluid_flow.radius_stator, + my_fluid_flow.omega, + my_fluid_flow.viscosity, + my_fluid_flow.length, + my_fluid_flow.load, + my_fluid_flow.radial_clearance, + ) + + sommerfeld_number(modified_s, my_fluid_flow.radius_stator, my_fluid_flow.length) + return + + +@app.cell +def _(my_fluid_flow, plot_eccentricity): + # Plotting the eccentricity + + plot_eccentricity(my_fluid_flow, scale_factor=0.5) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + The graphic above plots two circles: one representing the stator and one representing the rotor, considering the eccentricity. In this case, since the space between the stator and the rotor is very small, it is not seen in the graphic. + """) + return + + +@app.cell +def _(calculate_stiffness_and_damping_coefficients, my_fluid_flow): + # Getting the stiffness and damping matrices + + K, C = calculate_stiffness_and_damping_coefficients(my_fluid_flow) + return C, K + + +@app.cell +def _(C, K): + print(f"Kxx, Kxy, Kyx, Kyy = {K}") + print(f"Cxx, Cxy, Cyx, Cyy = {C}") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + The stiffness and damping matrices can be calculated analytically using the methods above. + """) + return + + +@app.cell +def _(my_fluid_flow, nz): + # Calculating pressure matrix + + my_fluid_flow.calculate_pressure_matrix_numerical()[int(nz / 2)] + return + + +@app.cell +def _(my_fluid_flow, nz, plot_pressure_theta): + # Plotting pressure along theta in a chosen z + + plot_pressure_theta(my_fluid_flow, z=int(nz / 2)) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/example_9.rst b/docs/user_guide/example_9.rst new file mode 100644 index 000000000..c2c998082 --- /dev/null +++ b/docs/user_guide/example_9.rst @@ -0,0 +1,5 @@ +Example 9 +========= + +.. marimo:: example_9.py + :height: 700px diff --git a/docs/user_guide/fluid_flow_elliptical_bearing.py b/docs/user_guide/fluid_flow_elliptical_bearing.py new file mode 100644 index 000000000..acbda28e0 --- /dev/null +++ b/docs/user_guide/fluid_flow_elliptical_bearing.py @@ -0,0 +1,136 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Fluid-flow: Elliptical Bearing + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Study below is based on {cite}`mota2020`, where is the complete theory used by *FluidFlow*: + + The elliptical bearing or "lemon bearing", as it is also known, is a variation of the cylindrical bearing with axial groove and reduced clearance in one direction. + + ![alt text](../_static/img/img_examplo_ff_eliptical.png) + + + + + For the inclusion of this new geometry, adaptations to the stator radius are necessary, as it will no longer be constant in $\theta$. As seen in the figure above, the new stator is composed of the arc $C_{1}$, with center in $O_{1}$, joined to the arc $C_{2}$, centered in $O_{2}$, both with radius $R_{o}$. In this new configuration, the centers are at a distance $\epsilon$ from the origin, called ellipticity. + + It is necessary to describe the stator from the origin. This new distance will be called $R_{o}^{*} $ and it varies along the angular position: + + $$R_o^* = \sqrt{R_o ^2 - \epsilon^2 \sin^2{\alpha}} + \epsilon \cos{\alpha}$$ + + where + $$\alpha =\begin{cases} + \pi/2 + \theta \text{,} + &\text{if} \quad \theta \in 1^{\circ} \text{quadrant} \\ + 3\pi/2 + \theta \text{,} + &\text{if} \quad \theta \in 2^{\circ} \text{quadrant} \\ + \theta - \pi/2 \text{,} + &\text{if} \quad \theta \in 3^{\circ} \text{quadrant} \\ + 5\pi/2 -\theta \text{,} + &\text{if} \quad \theta \in 4^{\circ} \text{quadrant} + \end{cases}$$ + . + + Another important parameter to be defined is the $ m $ preload which, in this text, will be established as: + + $$m = \dfrac{\epsilon}{F}$$ + + where $\epsilon$ is the ellipticity and $F=R_{o}-R_{i}$ is the radial clearance. + + For $m=0$, the bearing becomes cylindrical, while for $m \rightarrow 1$ the stator arcs tend to touch the axis. + """) + return + + +@app.cell +def _(): + import ross + from ross.bearings.fluid_flow_graphics import ( + plot_pressure_theta, + plot_pressure_surface, + ) + from ross.bearings.fluid_flow_coefficients import calculate_oil_film_force + from ross.bearings.fluid_flow_coefficients import find_equilibrium_position + from ross.bearings.fluid_flow_coefficients import ( + calculate_stiffness_and_damping_coefficients, + ) + + from ross.bearings.fluid_flow import fluid_flow_example3 + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + + my_fluid_flow_eliptical = fluid_flow_example3() + + fig1 = plot_pressure_theta( + my_fluid_flow_eliptical, z=int(my_fluid_flow_eliptical.nz / 2) + ) + fig1.show() + fig2 = plot_pressure_surface(my_fluid_flow_eliptical) + fig2.show() + + radial_force, tangential_force, force_x, force_y = calculate_oil_film_force( + my_fluid_flow_eliptical + ) + print("N=", radial_force) + print("T=", tangential_force) + print("fx=", force_x) + print("fy=", force_y) + + find_equilibrium_position(my_fluid_flow_eliptical) + print("(xi,yi)=", "(", my_fluid_flow_eliptical.xi, ",", my_fluid_flow_eliptical.yi, ")") + radial_force, tangential_force, force_x, force_y = calculate_oil_film_force( + my_fluid_flow_eliptical + ) + print("fx, fy=", force_x, ",", force_y) + + K, C = calculate_stiffness_and_damping_coefficients(my_fluid_flow_eliptical) + kxx, kxy, kyx, kyy = K[0], K[1], K[2], K[3] + cxx, cxy, cyx, cyy = C[0], C[1], C[2], C[3] + print("Stiffness coefficients:") + print("kxx, kxy, kyx, kyy = ", kxx, kxy, kyx, kyy) + print("Damping coefficients:") + print("cxx, cxy, cyx, cyy", cxx, cxy, cyx, cyy) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/fluid_flow_elliptical_bearing.rst b/docs/user_guide/fluid_flow_elliptical_bearing.rst new file mode 100644 index 000000000..0503856c3 --- /dev/null +++ b/docs/user_guide/fluid_flow_elliptical_bearing.rst @@ -0,0 +1,5 @@ +Fluid Flow Elliptical Bearing +============================= + +.. marimo:: fluid_flow_elliptical_bearing.py + :height: 700px diff --git a/docs/user_guide/fluid_flow_short_bearing.py b/docs/user_guide/fluid_flow_short_bearing.py new file mode 100644 index 000000000..41ccf6add --- /dev/null +++ b/docs/user_guide/fluid_flow_short_bearing.py @@ -0,0 +1,238 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Fluid-flow: Short Bearing + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + In the literature on bearings, several studies use the Reynolds equation, + + $$ \dfrac{\partial}{\partial{x}}\left(h^3\dfrac{\partial{p}}{\partial{x}}\right)+\dfrac{\partial}{\partial{z}}\left(h^3\dfrac{\partial{p}}{\partial{z}}\right) = 6 \mu \left\{ \left(U_o + U_1\right) \dfrac{\partial{h}}{\partial{x}} + 2 V \right\}$$ + + after a series of simplifications, to find the pressure behavior in bearings. However, as it is an equation that has no analytical solution, they use the artifice of approximating the equation for cases of short bearings $\left(L/D \rightarrow 0 \right)$ and infinitely long $\left(L/D \rightarrow \infty \right)$ (L length, D diameter). Thus, one of the parts of the equation is neglected, and it is possible to find reduced models that can be solved analytically. + + Most modern bearings in high performance turbomachinery applications have a small $L/D$ ratio, rarely exceeding the unit. The author indicates that the short model provides accurate results for cylindrical bearings with the ratio $L/D \leq 0.5$, being widely used for quick estimates of the performance characteristics of the static and dynamic forces of the bearing. + + In this context, the bearing length is considered to be very small and, according to {cite}`ishida2013linear`, the pressure variation in the $z$ direction can be considered much greater than in the $ x $ direction, that is, $\partial p/\partial x \ll \partial p/\partial z$. Thus, the first term of the Reynolds equation is neglected. Making the appropriate adjustments to the coordinate system adopted in this work, a formula is then obtained that describes the pressure behavior in the short bearing: + + $$ + p_{curto} = \dfrac{-3\mu \epsilon \omega \sin{\theta}}{\left(R_\theta - R_i\right)^2\left(1 + \epsilon \cos{\theta}\right)^3}\left[\left(z-\dfrac{L}{2}\right)^2 - \dfrac{L^2}{4}\right] + $$ + + where $\epsilon = \dfrac{e}{R_{o} - R_{i}}$ is the reason for eccentricity. + + The numerical solution presented is verified with this approximation, which is used by the Fluid-Flow code if the bearing is classified as short ($L/D \leq 1/4$) + """) + return + + +@app.cell +def _(): + import ross + from ross.bearings.fluid_flow_graphics import ( + plot_pressure_theta_cylindrical, + plot_pressure_z, + plot_pressure_theta, + plot_pressure_surface, + ) + from ross.bearings.fluid_flow import fluid_flow_example + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + + my_fluid_flow_short = fluid_flow_example() + my_fluid_flow_short.calculate_pressure_matrix_analytical() + + fig1 = plot_pressure_z(my_fluid_flow_short, theta=int(my_fluid_flow_short.ntheta / 2)) + fig1.show() + fig2 = plot_pressure_theta(my_fluid_flow_short, z=int(my_fluid_flow_short.nz / 2)) + fig2.show() + fig3 = plot_pressure_theta_cylindrical( + my_fluid_flow_short, z=int(my_fluid_flow_short.nz / 2) + ) + fig3.show() + return (my_fluid_flow_short,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **FORCES** + + For the approach of a short bearing, it is possible to perform the integrals analytically to obtain the forces of the oil film. These are given by {cite}`ishida2013linear` in the stationary context as: + + $$N = \dfrac{1}{2}\mu\left(\dfrac{R_i}{R_o - R_i}\right)^2 \dfrac{L^3}{r}\left[\dfrac{2\epsilon^2\omega}{\left(1-\epsilon^2\right)^2} \right]$$ + $$T = \dfrac{1}{2}\mu\left(\dfrac{R_i}{R_o - R_i}\right)^2 \dfrac{L^3}{r}\left[\dfrac{\pi\epsilon\omega}{2\left(1-\epsilon^2\right)^{3/2}} \right]$$ + """) + return + + +@app.cell +def _(my_fluid_flow_short): + from ross.bearings.fluid_flow_coefficients import calculate_oil_film_force + + radial_force, tangential_force, force_x, force_y = calculate_oil_film_force( + my_fluid_flow_short, force_type="short" + ) + print("N=", radial_force) + print("T=", tangential_force) + print("fx=", force_x) + print("fy=", force_y) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **EQUILIBRIUM POSITION** + + It is known that, in the equilibrium position, the vertical force $f_y$ tends to balance with the applied external load $W$. Thus, knowing $W$ and an equation of the force $f_y$ to approach the short bearing, it is possible to obtain the eccentricity of the rotor. According to {cite}`friswell2010dynamics`, this information is obtained by solving the quadratic polynomial in $\epsilon^2$ + + $$\epsilon^8 - 4\epsilon^6 + \left(6 - S_s^2\left(16 -\pi^2\right)\right)\epsilon^4 - \left(4 + \pi^2 S_s^2\right)\epsilon^2 +1=0$$ + + where $S_s=\dfrac{2R_o \omega \mu L^3}{8WF^2}$ is called the modified Sommerfeld number. + + Still according to {cite}`friswell2010dynamics`, the direction of the force given by: + + $$\tan{\beta}=\dfrac{\pi\sqrt{1 - \epsilon^2}}{4\epsilon}$$ + """) + return + + +@app.cell +def _(my_fluid_flow_short): + from ross.bearings.fluid_flow_geometry import ( + modified_sommerfeld_number, + calculate_eccentricity_ratio, + calculate_attitude_angle, + ) + + modified_s = modified_sommerfeld_number( + my_fluid_flow_short.radius_stator, + my_fluid_flow_short.omega, + my_fluid_flow_short.viscosity, + my_fluid_flow_short.length, + my_fluid_flow_short.load, + my_fluid_flow_short.radial_clearance, + ) + eccentricity_ratio = calculate_eccentricity_ratio(modified_s) + beta = calculate_attitude_angle(eccentricity_ratio) + print("Eccentricity ratio=", eccentricity_ratio) + print("Attitude angle=", beta) + print("(xi,yi)=", "(", my_fluid_flow_short.xi, ",", my_fluid_flow_short.yi, ")") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **DYNAMIC COEFFICIENTS** + + Once equations have been obtained that describe the forces $ f_x $ and $ f_y $ on the short bearing, it is possible to perform analytically the derivatives that define the stiffness and damping coefficients. {cite}`friswell2010dynamics` presents the stiffness and damping matrices as: + + $$K = \dfrac{W}{F}\begin{bmatrix} + k_{xx} & k_{xy}\\ + k_{yx} & k_{yy} + \end{bmatrix}\text{,}\quad + C = \dfrac{W}{F\omega}\begin{bmatrix} + c_{xx} & c_{xy}\\ + c_{yx} & c_{yy} + \end{bmatrix}$$ + + where + + $k_{xx} = 4 h_0 \left(\pi^2 \left(2 - \epsilon^2\right)+16\epsilon^2\right)\text{,}$ + + $k_{xy} = h_0 \dfrac{\pi \left(\pi^2 \left(1 - \epsilon^2\right)^2 - 16\epsilon^4\right)}{\epsilon\sqrt{1-\epsilon^2}}\text{,}$ + + $k_{yx} = - h_0 \dfrac{ + \pi \left( + \pi^2 + \left(1-\epsilon^2\right) + \left(1+2\epsilon^2\right) + +32\epsilon^2 + \left(1+\epsilon^2\right) + \right) } + {\epsilon\sqrt{1-\epsilon^2}}\text{,}$ + + $k_{yy} = 4 h_0 \left(\pi^2 \left(1 + 2\epsilon^2\right) + \dfrac{32\epsilon^2\left(1+\epsilon^2 \right)} + {1-\epsilon^2} \right)\text{,}$ + + $c_{xx} = h_0 \dfrac{2 \pi \sqrt{1 - \epsilon^2} + \left(\pi^2 \left(1 + 2\epsilon^2\right) + -16\epsilon^2 + \right)} + {\epsilon}\text{,}$ + + $c_{xy} = c_{yx} = - 8 h_0 \left(\pi^2 \left(1 + 2\epsilon^2\right) -16 \epsilon^2 \right)\text{,}$ + + $c_{yy} = h_0 \dfrac{2\pi\left(\pi^2\left(1 - \epsilon^2\right)^2 +48 \epsilon^2\right)}{\epsilon\sqrt{1 - \epsilon^2}}\text{,}$ + + and + + $h_0 = \dfrac{1}{\left(\pi^2\left(1 - \epsilon^2\right)+16\epsilon^2\right)^{3/2}}\text{.}$ + """) + return + + +@app.cell +def _(my_fluid_flow_short): + from ross.bearings.fluid_flow_coefficients import ( + calculate_short_stiffness_matrix, + calculate_short_damping_matrix, + ) + + [kxx, kxy, kyx, kyy] = calculate_short_stiffness_matrix(my_fluid_flow_short) + [cxx, cxy, cyx, cyy] = calculate_short_damping_matrix(my_fluid_flow_short) + + print("Stiffness coefficients:") + print("kxx, kxy, kyx, kyy = ", kxx, kxy, kyx, kyy) + print("Damping coefficients:") + print("cxx, cxy, cyx, cyy", cxx, cxy, cyx, cyy) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Most modern bearings in high-performance turbomachinery applications have a small $L/D$ ratio, rarely exceeding the unit. The author indicates that the short model provides accurate results for cylindrical bearings with the ratio $L/D \leq 0.5$, being widely used for quick estimates of the performance characteristics of the static and dynamic forces of the bearing. + + The results obtained by the Fluid Flow numerical solutions are compatible with these approaches for bearings with the ratio $L/D \leq 0.25$. However, it is worth mentioning that the features of Fluid Flow are not restricted to the context of short bearings, making it possible to explore other sizes and geometries. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/fluid_flow_short_bearing.rst b/docs/user_guide/fluid_flow_short_bearing.rst new file mode 100644 index 000000000..965e39079 --- /dev/null +++ b/docs/user_guide/fluid_flow_short_bearing.rst @@ -0,0 +1,5 @@ +Fluid Flow Short Bearing +======================== + +.. marimo:: fluid_flow_short_bearing.py + :height: 700px diff --git a/docs/user_guide/fluid_flow_theory.py b/docs/user_guide/fluid_flow_theory.py new file mode 100644 index 000000000..57380cd4b --- /dev/null +++ b/docs/user_guide/fluid_flow_theory.py @@ -0,0 +1,674 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Fluid-flow theory + + The FluidFlow code is responsible for providing Ross with simulations of thin thickness fluid in hydrodynamic bearings. It returns relevant information for the stability analysis of rotating machines, such as pressure field, fluid forces and dynamic coefficients. In this section, the main theoretical foundations of the modeling described in the code are synthesized and some examples are provided. + """) + return + + +@app.cell +def _(): + import ross + from ross.bearings.fluid_flow import fluid_flow_example2 + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + + my_fluid_flow = fluid_flow_example2() + return (my_fluid_flow,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **PROBLEM DESCRIPTION** + + Fluid flow occurs in the annular space between the shaft and the bearing, both of $ L $ length. These structures are called rotor and stator, respectively. The stator is fixed with radius $R_{o}$ and the rotor, with radius $R_{i} $, is a rigid body with rotation speed $\omega$, as shown in the figure below. + + ![alt text](../_static/img/img_examplo_ff_theory.png) + + + Due to the rotation of the rotor, a pressure field is set in the lubricating oil film, developing fluid forces that act on the rotor surface. For a constant speed of rotation, these forces displace the rotor to a location inside the stator called the _equilibrium position_. In this position, the stator and rotor are eccentric, with a distance between centers $e$ and an attitude angle $\beta$, formed between the axis connecting both centers and the vertical axis. + + Based on the eccentricity and attitude angle, the cosine law can be used to describe the position of the rotor surface $R_{\theta}$ with respect to the center of the stator: + + $$ R_{\theta} = \sqrt{R_i ^2 - e^2 \sin^2{\alpha}} + e \cos{\alpha},$$ + + where + $$\alpha =\begin{cases} + \dfrac{3\pi}{2} - \theta + \beta \text{,} + &\text{se } \dfrac{\pi}{2} + \beta \leq \theta < \dfrac{3\pi}{2} + \beta \\ \\ + - \left(\dfrac{3\pi}{2} - \theta + \beta\right) \text{,} + & \text{se } \dfrac{3\pi}{2} + \beta \leq \theta < \dfrac{5\pi}{2} + \beta + \end{cases}$$ + . + """) + return + + +@app.cell +def _(my_fluid_flow): + from ross.bearings.fluid_flow_graphics import plot_eccentricity + + fig = plot_eccentricity(my_fluid_flow, z=int(my_fluid_flow.nz / 2), scale_factor=0.5) + fig.show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **THEORETICAL MODELING** + + We start from the Navier Stokes and continuity equations: + + $$\rho \left(\dfrac{\partial \mathbf{v}}{\partial t} + \mathbf{v} \cdot \nabla \mathbf{v} \right) = \nabla \cdot \sigma$$ + + $$\dfrac{\partial \rho}{\partial t} + \nabla \cdot \left( \rho \mathbf{v} \right) = 0$$ + + where $\rho$ is the specific mass of the fluid, $\mathbf{v}$ is the velocity field, and $\sigma=-p \mathbf{I} + \tau$ is the Cauchy's tensor, in which $p$ represents the pressure field, $\tau$ is the stress tensor and $\mathbf{I}$ the identity tensor. In order to consider the effects of curvature, the velocity field is represented in cylindrical coordinates with $u$, $v$ and $w$ being the axial, radial and tangential speeds, respectively. + + In this code, the following hypotheses are considered: + + * Newtonian fluid: ${\mathbf{\tau}} = {\mathbf{\mu}}(\nabla \mathbf{v})$ + * Incompressible fluid: constant $\rho$ + * Permanent regime: $\dfrac{\partial(*)}{\partial t}=0$ + + Thus, the equations can be rewritten as + + $$\rho \left(\mathbf{v} \cdot \nabla \mathbf{v}\right) = - \nabla p + \mu \nabla^2 \mathbf{v}$$ + $$\nabla \cdot \mathbf{v}=0,$$ + + or in terms of each direction: + + + * Direction $z$ (similar to directions $r$ and $\theta$): + + $${\rho + \left( + u \dfrac{\partial{u}}{\partial{z}} + + v \dfrac{\partial{u}}{\partial{r}} + + \dfrac{w}{r} \dfrac{\partial{u}}{\partial{\theta}} + \right)} + = + {-\dfrac{\partial{p}}{\partial{z}} + + \mu + \left( + \dfrac{1}{r} \dfrac{\partial{}}{\partial{r}}\left[r\dfrac{\partial{u}}{\partial{r}} \right] + + \dfrac{1}{r^2}\dfrac{\partial^2{u}}{\partial{\theta ^2}} + + \dfrac{\partial^2{u}}{\partial{z^2}} + \right)}$$ + + * Continuity: + + $$\dfrac{1}{r} \dfrac{\partial{\left(rv\right)}}{\partial{r}}+\dfrac{1}{r}\dfrac{\partial{w}}{\partial{\theta}}+\dfrac{\partial{u}}{\partial{z}} = 0$$ + + **Dimensionaless Analysis** + + Considering U and L as a typical speed and sizes with the relation + + $$(R_{o}-R_{i}) = F \ll L,$$ + + the equation that represents movement in the $z$ direction, in its dimensionless form, is: + + $${\rho + \left( + U\hat{u} \dfrac{\partial{U\hat{u}}}{\partial{L\hat{z}}} + + U\hat{v} \dfrac{\partial{U\hat{u}}}{\partial{F\hat{r}}} + + \dfrac{U\hat{w}}{L\hat{r}} \dfrac{\partial{U\hat{u}}}{\partial{\theta}} + \right)} + = + {-\dfrac{\partial{P\hat{p}}}{\partial{L\hat{z}}} + + \mu + \left( + \dfrac{1}{L\hat{r}} \dfrac{\partial{}}{\partial{F\hat{r}}}\left[L\hat{r}\dfrac{\partial{U\hat{u}}}{\partial{F\hat{r}}} \right] + + \dfrac{1}{L^2\hat{r}^2}\dfrac{\partial^2{U\hat{u}}}{\partial{\theta ^2}} + + \dfrac{\partial^2{U\hat{u}}}{\partial{L^2\hat{z}^2}} + \right)}$$ + + where the dimensionless quantities aare denoted with a circumflex accent. + + The previous equation is rearranged to explicit the Reynolds number $\left(\mathbf{Re}=\dfrac{\rho U L}{\mu}\right)$ by using the relation $P = \dfrac{\mu UL}{F^2}$: + + $${ \mathbf{Re} + \left( + \left(\dfrac{F^2}{L^2}\right)\hat{u} \dfrac{\partial{\hat{u}}}{\partial{\hat{z}}} + + \left(\dfrac{F}{L} \right) \hat{v} \dfrac{\partial{\hat{u}}}{\partial{\hat{r}}} + + \left(\dfrac{F^2}{L^2}\right) \dfrac{\hat{w}}{\hat{r}} \dfrac{\partial{\hat{u}}}{\partial{\theta}} + \right)} + = + {-\dfrac{\partial{\hat{p}}}{\partial{\hat{z}}} + + \left( + \dfrac{1}{\hat{r}} \dfrac{\partial{}}{\partial{\hat{r}}}\left[\hat{r}\dfrac{\partial{\hat{u}}}{\partial{\hat{r}}} \right] + + \left(\dfrac{F^2}{L^2}\right) \dfrac{1}{\hat{r}^2}\dfrac{\partial^2{\hat{u}}}{\partial{\theta ^2}} + + \left(\dfrac{F^2}{L^2}\right)\dfrac{\partial^2{\hat{u}}}{\partial{\hat{z}^2}} + \right)}$$ + + After some simplifications, based on lubrication theory, the equations along each direction are: + + * $z$ direction: + + $$-\dfrac{\partial{\hat{p}}}{\partial{\hat{z}}} + + \dfrac{1}{\hat{r}} \dfrac{\partial{}}{\partial{\hat{r}}}\left(\hat{r}\dfrac{\partial{\hat{u}}}{\partial{\hat{r}}} \right) =0$$ + + * $r$ direction: + + $$-\dfrac{\partial{\hat{p}}}{\partial{\hat{r}}}=0$$ + + * $\theta$ direction: + + $$\dfrac{\partial{}}{\partial{\hat{r}}} + \left(\dfrac{1}{\hat{r}}\dfrac{\partial{(\hat{r}\hat{w})}}{\partial{\hat{r}}}\right) + - \dfrac{1}{\hat{r}}\dfrac{\partial{\hat{p}}}{\partial{\theta}} + =0$$ + + Returning to the dimensional form and noting that the pressure does not change along the radial direction, the simplified Reynold's equations are: + + $$-\dfrac{\partial{p}}{\partial{z}} + + \mu \left[\dfrac{1}{r} \dfrac{\partial{}}{\partial{r}}\left(r\dfrac{\partial{u}}{\partial{r}} \right)\right] =0$$ + $$-\dfrac{1}{r} \dfrac{\partial{p}}{\partial{\theta}} + + \mu \left[\dfrac{\partial{}}{\partial{r}}\left(\dfrac{1}{r}\dfrac{\partial{(rw)}}{\partial{r}}\right)\right] + =0$$ + + **Speeds** + + It is now possible to integrate the above equations to find the velocities in the $z$ (axial velocity $u$) and $\theta$ (tangential velocity w) directions. This yields: + + $$u = \dfrac{1}{\mu}\left[\dfrac{\partial{p}}{\partial{z}}\dfrac{r^2}{4} + c_1 \ln{r} + c_2\right]$$ + + $$w = \dfrac{1}{\mu} \left\{\dfrac{1}{2}\left[\dfrac{\partial{p}}{\partial{\theta}} r \left(\ln{r} -\dfrac{1}{2}\right) + c_3 r\right] + \dfrac{c_4}{r}\right\}$$ + + where $c_1$, $c_2$, $c_3$ and $c_4$ are constant in the integration in the variable $r$. + + By applying the boundary conditions + + * $u(R_{o})=u(R_{\theta})=0,$ + * $w(R_{o})=0$ and $w(R_{\theta}) = \omega R_{i},$ + + the speeds are + + $$u = + \dfrac{1}{4\mu} + \dfrac{\partial{p}}{\partial{z}} R_{\theta}^2 + \left[ + \left( + \dfrac{r}{R_{\theta}} + \right)^2 + - \dfrac{\left(R_{o}^2 - R_{\theta}^2\right)}{R_{\theta}^2 \ln{\left(\dfrac{R_{o}}{R_{\theta}}\right)}} + \left( + \ln{\dfrac{r}{R_{\theta}}} + \right) + - 1 + \right]$$ + $$w = + \dfrac{1}{2 \mu} + \dfrac{\partial p}{\partial \theta} + \left[ + r \left( + \ln r - \dfrac{1}{2} + \right) + + k r + - \dfrac{R_{o}^2}{r} \left( + \ln R_{o} + k - \dfrac{1}{2} + \right) + \right] + + \dfrac{\omega R_{i} R_{\theta}}{\left(R_{\theta}^2 - R_{o}^2\right)} + \left( + r - \dfrac{R_{o}^2}{r} + \right)$$ + + where $k = + \frac{1}{R^2_{\theta}-R^2_{o}} + \left[ + R^2_{o} + \left( + \ln R_{o} - \frac{1}{2} + \right) + -R^2_{\theta} + \left( + \ln R_{\theta} - \frac{1}{2} + \right) + \right]$ + + **Pressure** + + Once the speeds are calculated, the continuity equation is integrated into the annular region of interest, from $R_{\theta}$ to $R_o$: + + $$ \int^{R_{o}}_{R_{\theta}} + \left( + \frac{\partial{(rv)}}{\partial{r}} + + \frac{\partial{w}}{\partial{\theta}} + + \frac{\partial{(ru)}}{\partial{z}} + \right) + \,dr = 0$$ + + The integral is splitted into three integrals and the fundamental theorem of calculus and Leibnitz rule are applied. This yields: + + 1. $\int^{R_{o}}_{R_{\theta}} + \left( + \frac{\partial{(rv)}}{\partial{r}} + \right) \,dr + = + R_{o} v(R_{o}) - R_{\theta} v(R_{\theta})$ + 2. $\int^{R_{o}}_{R_{\theta}} + \left( + \frac{\partial{w}}{\partial{\theta}} + \right) \, dr + = + \dfrac{\partial}{\partial \theta} + \int^{R_{o}}_{R_{\theta}} w \,dr + - \left[ + w(R_{o}) \dfrac{\partial R_{o}}{\partial \theta} + - w(R_{\theta}) \dfrac{\partial R_{\theta}}{\partial \theta} + \right]$ + 3. $\int^{R_{o}}_{R_{\theta}} + \left( + \frac{\partial{(ru)}}{\partial{z}} + \right) \, dr + = + \dfrac{\partial}{\partial z} + \int^{R_{o}}_{R_{\theta}} (ru) \,dr + - \left[ + u(R_{o}) \dfrac{\partial R_{o}}{\partial z} + - u(R_{\theta}) \dfrac{\partial R_i}{\partial z} + \right]$ + + Here, some considerations are taken into account: + + * The radial velocity is zero at $v(R_{o})=0$. However, $v(R_{\theta})\neq 0$ because the origin of the frame is not in the center of the rotor. + * As seen earlier, $w(R_o) = 0$ and $w(R_{\theta}) = \omega R_{i}$. Due to the eccentricity, $\dfrac{\partial R_{\theta}}{\partial \theta} \neq 0$. + * By the boundary condition it is known that $u(R_o)=u(R_{\theta})=0$. + + Moreover, the speeds $v(R_{\theta})$ and $w(R_{\theta})$ must be calculated with kinematic relations. First, consider any $ A $ point pertaining to the rotor surface. Due to rotation, point $A$ has a velocity + + $$v_{rot} = v_{rad}\,e_{r} + v_{tan}\,e_{\theta},$$ + + where $e_{r}$ and $e_{\theta}$ are unit vectors of the cylindrical coordinate system. This is shown in the figure below. + + ![alt text](../_static/img/img_example_ff_theory2.png) + + + Now, consider the position vector $a=R_{\theta} e_r$, from the stator center to any point in the rotor surface. Its time derivative, relative to an inertial frame, is the total speed $v_{tot}$: + + $$v_{rot}=\omega \dfrac{\partial R_{\theta}}{\partial \theta}\,e_r + + \omega R_{\theta}\,e_{\theta}$$ + + where $v(R_{\theta}) = \omega \dfrac{\partial R_{\theta}}{\partial \theta}$ and $w(R_{\theta}) = \omega R_{\theta}$. + + Substituting the values of $v(R_{\theta})$ and $w(R_{\theta})$ in the continuity equation, we obtain: + + $$\dfrac{\partial}{\partial \theta} + \int^{R_{o}}_{R_{\theta}} w \,dr + + \dfrac{\partial}{\partial z} + \int^{R_{o}}_{R_{\theta}} ru \,dr + = 0$$ + + Performing this integral and replacing the $u$ and $w$ speeds yields: + + $$\dfrac{\partial}{\partial \theta} + \left( + \mathbf{C_1} + \dfrac{\partial p}{\partial \theta} + \right) + + + \dfrac{\partial}{\partial z} + \left( + \mathbf{C_2} + \dfrac{\partial p}{\partial z} + \right) + = + \dfrac{\partial}{\partial \theta} + \mathbf{C_0}$$ + + where + + $$\mathbf{C_0} = + - \omega R_{i} R_{\theta} + [ + \ln{\left(\frac{R_{o}}{R_{\theta}}\right)} + (1 + \frac{R_{\theta}^2}{(R_{o}^2-R_{\theta}^2)}) + -\dfrac{1}{2} + ] $$ , + + + $$\mathbf{C_1} = + \dfrac{1}{4\mu} + {[R_{o}^2 \ln{R_{o}} + - R_{\theta}^2 \ln{R_{\theta}} + + (R_{o}^2-R_{\theta}^2)(k-1) + ] + - 2R_{o}^2 + [ + (\ln{R_{o}}+k-\dfrac{1}{2}) + \ln{(\frac{R_{o}}{R_{\theta}})} + ] + } + $$ , + + $$\mathbf{C_2} = + - \dfrac{R_{\theta}^2}{8\mu} + { + [ + R_{o}^2-R_{\theta}^2 + -\dfrac{(R_{o}^4-R_{\theta}^4)}{2R_{\theta}^2} + ] + + + ( + \dfrac{R_{o}^2-R_{\theta}^2}{R_{\theta}^2 \ln{\left(\dfrac{R_{o}}{R_{\theta}}\right)}} + ) + [ + R_{o}^2\ln{(\dfrac{R_{o}}{R_{\theta}})} + -\dfrac{(R_{o}^2-R_{\theta}^2)}{2} + ] + }$$ + + This is a elliptic partial differential equation and its solution detemines the pressure field $p$. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **NUMERICAL SOLUTION** + + The partial differential equation is solved using finite centered differences method. It is applied to a regular rectangular mesh with $𝑁_{z}$ nodes in the axial direction and $𝑁_{\theta}$ nodes in the tangential direction, as shown in the figure below. + + ![alt text](../_static/img/img_example_ff_theory3.png) + + + The discretized equation is given by + + $$p_{i-1,j}\frac{(\mathbf{C_{2(i-1,j)}})}{\Delta z^{2}} + p_{i,j-1}\frac{(\mathbf{C_{1(i,j-1)}})}{\Delta\theta^{2}} - p_{i,j}\left(\frac{(\mathbf{C_{1(i,j)}} + \mathbf{C_{1(i,j-1)}})}{\Delta\theta^{2}} + \frac{(\mathbf{C_{2(i,j)}} + \mathbf{C_{2(i-1,j)}})}{\Delta z^{2}} \right) + p_{i,j+1} \frac{(\mathbf{C_{1(i,j)}})}{\Delta\theta^{2}} + p_{i+1,j}\frac{(\mathbf{C_{2(i,j)}})}{\Delta z^{2}} = \frac{1}{\Delta\theta}\left[\mathbf{C_{0W(i,j)}}-\mathbf{C_{0W(i,j-1)}}\right],\nonumber$$ + + with boundary conditions + + $$p(z=0)=p(1,j)=P_{in}$$ + $$p(z=L)=p(NZ,j)=P_{out}$$ + $$p(\theta=0)=p(\theta=2\pi)=p(i,1)=p(i,N\theta)$$ + + **Cavitation Condition** + + According to DOWNSON and TAYLOR (1979) [1], cavitation can be defined as the phenomenon that describes the discontinuity of a fluid due to the existence of gases or steam. This is a characteristic phenomenon in hydrodynamic bearings. + + It is important to note that this change in pressure behavior due to cavitation does not necessarily start at the point of least thickness in the annular space. Several studies seek to establish the appropriate boundary conditions to describe the beginning of cavitation in the fluid. ISHIDA and YAMAMOTO (2012) [2] indicate that the condition of Gumbel is widely used because of its simplicity. Using the argument that lubricant evaporation and axial air flow from both ends can occur, the pressure in the region $\pi < \theta < 2\pi$ is considered to be almost zero (that is, the atmospheric pressure ). $p = 0 $ is then defined across the divergent region. + + ![alt text](../_static/img/img_example_ff_theory4.png) + + + In addition, according to SANTOS (1995) [3], although it violates mass conservation, this condition presents acceptable errors in the global parameters of the bearing. For these reasons, the present study adopts the Gumbel boundary condition to describe the phenomenon of cavitation. + """) + return + + +@app.cell +def _(my_fluid_flow): + from ross.bearings.fluid_flow_graphics import ( + plot_pressure_theta_cylindrical, + plot_pressure_z, + plot_pressure_theta, + plot_pressure_surface, + ) + + my_fluid_flow.calculate_pressure_matrix_numerical() + fig1 = plot_pressure_z(my_fluid_flow, theta=int(my_fluid_flow.ntheta / 2)) + fig1.show() + fig2 = plot_pressure_theta(my_fluid_flow, z=int(my_fluid_flow.nz / 2)) + fig2.show() + fig3 = plot_pressure_theta_cylindrical(my_fluid_flow, z=int(my_fluid_flow.nz / 2)) + fig3.show() + fig4 = plot_pressure_surface(my_fluid_flow) + fig4.show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **FORCES** + + The pressure field developes forces that acts on the rotor surface. According to SAN ANDRES (2010) [4] these forces are given by: + + $$ \begin{bmatrix} + N \\ T + \end{bmatrix} + = R_i \int_{0}^{L} \int_{0}^{2\pi} p \begin{bmatrix} + \cos{\theta}\\\sin{\theta} + \end{bmatrix} d\theta dz$$ + + As the pressure behavior is obtained numerically, the integrals above also need a numerical method to be solved. For this, the composite Simpson rule applied through the *integrate.simpson* method of the library *SciPy* was chosen, by VIRTANEN et al. (2020) [5]. + + It is also possible to obtain the forces $ f_x $ and $ f_y $ in the Cartesian coordinate system: + + $$ f_x = T \cos{(\beta)} - N \sin{(\beta)} + \\ + f_y = T \sin{(\beta)} + N \cos{(\beta)}$$ + """) + return + + +@app.cell +def _(my_fluid_flow): + from ross.bearings.fluid_flow_coefficients import calculate_oil_film_force + (_radial_force, _tangential_force, _force_x, _force_y) = calculate_oil_film_force(my_fluid_flow) + print('N=', _radial_force) + print('T=', _tangential_force) + print('fx=', _force_x) + print('fy=', _force_y) + return (calculate_oil_film_force,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **EQUILIBRIUM POSITION** + + As indicated by SAN ANDRES (2010) [4], there is an equilibrium position in which the rotor center is stationary and the fluid film forces balance the external forces, in this case the rotor weight. This is expressed as: + + $$f_{x_0} = 0$$ + $$f_{y_0} = W $$ + + Knowing the external load $ W $, it is possible to reach the equilibrium position using an iterative method. Starting at an initial position, the residues between the forces at the current position and external forces are calculated. If the residue is greater than a defined tolerance, the position of the rotor is varied systematically, inside the fourth quadrant, until the desired tolerance is reached. In other words, a local minimum of the forces function is reached. The Python tool *optimize.least\_squares* was used for this purpose. The method is shown in the image below. + + ![alt text](../_static/img/img_example_ff_theory5.png) + + """) + return + + +@app.cell +def _(calculate_oil_film_force, my_fluid_flow): + from ross.bearings.fluid_flow_coefficients import find_equilibrium_position + find_equilibrium_position(my_fluid_flow) + print('(xi,yi)=', '(', my_fluid_flow.xi, ',', my_fluid_flow.yi, ')') + (_radial_force, _tangential_force, _force_x, _force_y) = calculate_oil_film_force(my_fluid_flow) + print('fx, fy=', _force_x, ',', _force_y) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **DYNAMIC COEFFICIENTS** + + According to SAN ANDRÉS (2010) [4], small disturbances around the equilibrium position allow us to express the fluid film forces as an expansion in Taylor series: + + $$f_x = f_{x_0} + + \dfrac{\partial f_x}{\partial x} \Delta x + + \dfrac{\partial f_x}{\partial y} \Delta y + + \dfrac{\partial f_x}{\partial \dot{x}} \Delta \dot{x} + + \dfrac{\partial f_x}{\partial \dot{y}} \Delta \dot{y} + \\ + f_y = f_{y_0} + + \dfrac{\partial f_y}{\partial x} \Delta x + + \dfrac{\partial f_y}{\partial y} \Delta y + + \dfrac{\partial f_y}{\partial \dot{x}} \Delta \dot{x} + + \dfrac{\partial f_y}{\partial \dot{y}} \Delta \dot{y}$$ + + From this expansion, the author also defines the stiffness coefficients $(k_{ij})$, damping $(c_{ij})$ as: + + $$ k_{ij}=-\dfrac{\partial f_i}{\partial j}\text{;} + \quad \quad + c_{ij}=-\dfrac{\partial f_i}{\partial \dot{j}}\text{;} + \quad \quad + i,j \in x,y$$ + + The procedure to calculate these coeficients is divided in three steps. First, two perturbations along the $x$ and $y$ directions are considered in the calculation of the velocity of any point $A$ in the rotor surface. This is expressed as + + $$v_{tot} = v_{rot} + v_p$$ + + where $v_{rot}$ is the velocity of the point due to the rotation and $v_{p}$ is the velocity acquired by a disturbance along each direction. These perturbation are considered of the type + + $$(v_p)_x = \omega_p x_p \cos{(\omega_p t)}e_x \longrightarrow \text{for a perturbation along the $x$ direction} + \\ + (v_p)_y = \omega_p y_p \cos{(\omega_p t)}e_y \longrightarrow \text{for a perturbation along the $y$ direction},$$ + + where $e_x$ and $e_y$ are unit vectors in cartesian coordinates. + + Combination of the two last equations, in cylindrical coordinates, gives the following velocity along each axis: + + $$(v_{tot})_x + = \underset{v(R_\theta)_x}{\underbrace{\left(\omega \dfrac{\partial R_{\theta}}{\partial \theta} + \omega_p x_p \cos{(\omega_p t)\cos{(\theta)}}\right)}} e_r + + \underset{w(R_\theta)_x}{\underbrace{\left(\omega R_{\theta} - \omega_p x_p \cos{(\omega_p t)}\sin{(\theta)} \right)}} e_{\theta} + \\ + (v_{tot})_y + = \underset{v(R_\theta)_y}{\underbrace{\left(\omega \dfrac{\partial R_{\theta}}{\partial \theta} + \omega_p y_p \cos{(\omega_p t)\sin{(\theta)}}\right)}} e_r + + \underset{w(R_\theta)_y}{\underbrace{\left(\omega R_{\theta} - \omega_p y_p \cos{(\omega_p t)}\cos{(\theta)} \right)}} e_{\theta}$$ + + The second step is to substitute the radial speeds ($v(R_\theta)_x$ and $v(R_\theta)_y$), and tangential speeds($w(R_\theta)_x$ and $w(R_\theta)_y$) in the continuity equation, previously calculated for a fixed position. Thus, when the rotor is rotating and its center is being perturbed along horizontal and vertical directions, the new $C_0$ coefficient of the continuity equation is + + $$(C_0)_x = C_0 + \left[R_{\theta} \omega_p x_p \cos{(\omega_p t)} \sin{(\theta)}\right] + \\ + (C_0)_y = C_0 + \left[R_{\theta} \omega_p y_p \cos{(\omega_p t)} \cos{(\theta)}\right]$$ + + This new coefficient ensures that the fluid film forces are calculated for a rotor with a fixed rotation $\omega$ and with a center moving along a horizontal or vertical direction with a fixed frequency $\omega_p$. + + The final step is to calculate the forces, along both the horizontal and vertical directions, for each perturbation. This sets the following four equations: + + $$\left( f_x[t] \right)_x = f_{x_0} + k_{xx}\Delta x[t] + c_{xx}\Delta \dot{x}[t]\text{,} + \\ + \left( f_y[t] \right)_x = f_{y_0} + k_{yx}\Delta x[t] + c_{yx}\Delta \dot{x}[t]\text{,} + \\ + \left( f_x[t] \right)_y = f_{x_0} + k_{xy}\Delta y[t] + c_{xy}\Delta \dot{y}[t]\text{,} + \\ + \left( f_y[t] \right)_y = f_{y_0} + k_{yy}\Delta y[t] + c_{yy}\Delta \dot{y}[t] $$ + + where the only unkowns are the eight coefficients. + In matrix form, this equation is expressed as follows + + $$\mathbf{A}_x + \begin{bmatrix} + k_{xx} & k_{yx}\\ + c_{xx} & c_{yx} + \end{bmatrix} + = \mathbf{F}_x + \quad \quad \text{e} \quad \quad + \mathbf{A}_y + \begin{bmatrix} + k_{xy} & k_{yy}\\ + c_{xy} & c_{yy} + \end{bmatrix} + = \mathbf{F}_y$$ + + The coefficients are obtained by using the Moore Penrose inverse and solving for each set of coefficients: + + $$\begin{bmatrix} + k_{xx} & k_{yx}\\ + c_{xx} & c_{yx} + \end{bmatrix} + = \left( \mathbf{A}_{x}^{T} \mathbf{A}_{x}\right)^{-1} + \mathbf{A}_{x}^{T} \mathbf{F}_x \quad \quad \text{e} \quad \quad + \begin{bmatrix} + k_{xy} & k_{yy}\\ + c_{xy} & c_{yy} + \end{bmatrix} + = \left( \mathbf{A}_{y}^{T} \mathbf{A}_{y}\right)^{-1} + \mathbf{A}_{y}^{T}\mathbf{F}_y$$ + """) + return + + +@app.cell +def _(my_fluid_flow): + from ross.bearings.fluid_flow_coefficients import ( + calculate_stiffness_and_damping_coefficients, + ) + + K, C = calculate_stiffness_and_damping_coefficients(my_fluid_flow) + kxx, kxy, kyx, kyy = K[0], K[1], K[2], K[3] + cxx, cxy, cyx, cyy = C[0], C[1], C[2], C[3] + print("Stiffness coefficients:") + print("kxx, kxy, kyx, kyy = ", kxx, kxy, kyx, kyy) + print("Damping coefficients:") + print("cxx, cxy, cyx, cyy", cxx, cxy, cyx, cyy) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Based on MOTA (2020), where is the complete theory used by *FluidFlow*: + + Mota, J. A.; **Estudo da teoria de lubrificação com parametrização diferenciada da geometria e aplicações em mancais hidrodinâmicos.** Dissertação de Mestrado - Programa de Pós-Graduação em Informática, Universidade Federal do Rio de Janeiro, Rio de Janeiro, 2020 + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **REFERENCES CITED IN THE TEXT** + + [1] DOWSON, D.; TAYLOR, C. Cavitation in bearings. **Annual Review of Fluid Mechanics**, [S.l.], v. 11, n. 1, p. 35–65, 1979. + + [2] ISHIDA, Y.; YAMAMOTO, T. Flow-Induced Vibrations. In: **Linear and nonlinear rotordynamics**. Weinheim: Wiley Online Library, 2012. p. 235–261. + + [3] SANTOS, E. S. ** Carregamento dinamico de mancais radiais com cavitaçãodo filme de óleo**. Dissertação de Mestrado — Centro Tecnológico, Universidade Federal de Santa Catarina, Santa Catarina, 1995. + + [4] SAN ANDRES, L. **Experimental Identification of Bearing Force Coefficients**, Notes 1 5 Acesso em 08/02/2020, http://oaktrust.library.tamu.edu/handle/1969.1/93199. + + [5] VIRTANEN, P. et al. SciPy 1.0: fundamental algorithms for scientific computing inpython. **Nature Methods**, [S.l.], v. 17, p. 261–272, 2020. + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/fluid_flow_theory.rst b/docs/user_guide/fluid_flow_theory.rst new file mode 100644 index 000000000..27640b545 --- /dev/null +++ b/docs/user_guide/fluid_flow_theory.rst @@ -0,0 +1,5 @@ +Fluid Flow Theory +================= + +.. marimo:: fluid_flow_theory.py + :height: 700px diff --git a/docs/user_guide/fluid_flow_wear_bearing.py b/docs/user_guide/fluid_flow_wear_bearing.py new file mode 100644 index 000000000..5547c09bb --- /dev/null +++ b/docs/user_guide/fluid_flow_wear_bearing.py @@ -0,0 +1,132 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Fluid-flow: Wear Bearing + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **WEAR BEARINGS** + + Although lubrication reduces the friction between the metal surfaces of the bearing, these structures usually suffer wear after a long operating period or else due to a certain number of repetitions of the starting cycles. + + ![alt text](../_static/img/img_examplo_ff_wear.png) + + + + + The wear geometry that will be used in the FluidFlow has been adapted from the version presented by MACHADO; CAVALCA (2015) [1]. To include wear in the geometry, it is necessary to make some adaptations to the stator radius. Considering that the fault starts at the angular position $\theta = \theta_{s}$ and ends at $\theta = \theta_{f}$, the stator description from the origin is defined as: + + $$R_o^* = R_o + d_{\theta}$$ + + where + $$d_{\theta} =\begin{cases} + 0 \text{,} + &\text{if} + \quad 0 \leq \theta \leq \theta_s\text{,} \quad + \theta_f \leq \theta \leq 2\pi \\ + d_0 - F \left(1 + \cos{\left(\theta - \pi/2\right)} \right) \text{,} + &\text{if} + \quad \theta_s < \theta < \theta_f + \end{cases}$$ + . + + In $\theta_{s}$ and $\theta_{f}$, the wear depth is zero, so the location of the edges can be defined as follows: + + $$\theta_s = \pi/2 + \cos^{-1}{\left(d_0/F -1\right)} + \gamma \nonumber\\ + \theta_f = \pi/2 - \cos^{-1}{\left(d_0/F -1\right)} + \gamma $$ + """) + return + + +@app.cell +def _(): + import ross + from ross.bearings.fluid_flow_graphics import ( + plot_pressure_theta, + plot_pressure_surface, + ) + from ross.bearings.fluid_flow_coefficients import calculate_oil_film_force + from ross.bearings.fluid_flow_coefficients import find_equilibrium_position + from ross.bearings.fluid_flow_coefficients import ( + calculate_stiffness_and_damping_coefficients, + ) + + from ross.bearings.fluid_flow import fluid_flow_example4 + import plotly.graph_objects as go + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + + my_fluid_flow_wear = fluid_flow_example4() + + fig8 = plot_pressure_theta(my_fluid_flow_wear, z=int(my_fluid_flow_wear.nz / 2)) + fig8.show() + fig9 = plot_pressure_surface(my_fluid_flow_wear) + fig9.show() + + radial_force, tangential_force, force_x, force_y = calculate_oil_film_force( + my_fluid_flow_wear + ) + print("N=", radial_force) + print("T=", tangential_force) + print("fx=", force_x) + print("fy=", force_y) + + find_equilibrium_position(my_fluid_flow_wear) + print("(xi,yi)=", "(", my_fluid_flow_wear.xi, ",", my_fluid_flow_wear.yi, ")") + radial_force, tangential_force, force_x, force_y = calculate_oil_film_force( + my_fluid_flow_wear + ) + print("fx, fy=", force_x, ",", force_y) + + K, C = calculate_stiffness_and_damping_coefficients(my_fluid_flow_wear) + kxx, kxy, kyx, kyy = K[0], K[1], K[2], K[3] + cxx, cxy, cyx, cyy = C[0], C[1], C[2], C[3] + print("Stiffness coefficients:") + print("kxx, kxy, kyx, kyy = ", kxx, kxy, kyx, kyy) + print("Damping coefficients:") + print("cxx, cxy, cyx, cyy", cxx, cxy, cyx, cyy) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Based on MOTA (2020), where is the complete theory used by *FluidFlow*: + + Mota, J. A.; **Estudo da teoria de lubrificação com parametrização diferenciada da geometria e aplicações em mancais hidrodinâmicos.** Dissertação de Mestrado - Programa de Pós-Graduação em Informática, Universidade Federal do Rio de Janeiro, Rio de Janeiro, 2020 + + + + **REFERENCES CITED IN THE TEXT** + + [1] MACHADO, T. H.; CAVALCA, K. L. Modeling of hydrodynamic bearing wear in rotor-bearing systems. **Mechanics Research Communications** - Elsevier, [S.l.],v. 69, p. 15–23, 2015. + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/fluid_flow_wear_bearing.rst b/docs/user_guide/fluid_flow_wear_bearing.rst new file mode 100644 index 000000000..ffc8ec63b --- /dev/null +++ b/docs/user_guide/fluid_flow_wear_bearing.rst @@ -0,0 +1,5 @@ +Fluid Flow Wear Bearing +======================= + +.. marimo:: fluid_flow_wear_bearing.py + :height: 700px diff --git a/docs/user_guide/tutorial_part_1.py b/docs/user_guide/tutorial_part_1.py new file mode 100644 index 000000000..3db45d332 --- /dev/null +++ b/docs/user_guide/tutorial_part_1.py @@ -0,0 +1,1619 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App(auto_download=["ipynb"]) + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Tutorial - Modeling + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + This is a basic tutorial on how to use ROSS (rotordynamics open-source software), a Python library for rotordynamic analysis. Most of this code follows object-oriented paradigm, which is represented in this + [UML DIAGRAM](https://user-images.githubusercontent.com/32821252/50386686-131c5200-06d3-11e9-9806-f5746295be81.png). + + Also [**ROSS GPT**](https://bit.ly/rossgpt), is a virtual assistant trained specifically for the ROSS package. + + Before starting the tutorial, it is worth noting some of ROSS' design characteristics. + + First, we can divide the use of ROSS in two steps: + - Building the model; + - Calculating the results. + + We can build a model by instantiating elements such as beams (shaft), disks and bearings. These elements are all defined in classes with names such as `ShaftElement`, `BearingElement` and so on. + + After instantiating some elements, we can then use these to build a rotor. + + This tutorial is about building your **rotor model**. First, you will learn how to create and assign **materials**, how to instantiate the **elements** which compose the rotor and how to convert **units** in ROSS with [pint](https://pint.readthedocs.io/en/stable/) library. This means that every time we call a function, we can use pint.Quantity as an argument for values that have units. If we give a float to the function ROSS will consider SI units as default. + + In the following topics, we will discuss the most relevant classes for a quick start on how to use ROSS. + """) + return + + +@app.cell +def _(): + from pathlib import Path + # import ross as rs + import numpy as np + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return Path, np + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 1: Material Class + + There is a class called Material to hold material's properties. Materials are applied to shaft and disk elements. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.1 Creating a material + + To instantiate a Material class, you only need to give 2 out of + the following parameters: `E` (Young's Modulus), `G_s` (Shear + Modulus) ,`Poisson` (Poisson Coefficient), and the material + density `rho`. + """) + return + + +@app.cell +def _(rs): + # from E and G_s + steel = rs.Material(name="Steel", rho=7810, E=211e9, G_s=81.2e9) + # from E and Poisson + steel2 = rs.Material(name="Steel", rho=7810, E=211e9, Poisson=0.3) + # from G_s and Poisson + steel3 = rs.Material(name="Steel", rho=7810, G_s=81.2e9, Poisson=0.3) + + print(steel) + + # returning attributes + print("=" * 36) + print(f"Young's Modulus: {steel.E}") + print(f"Shear Modulus: {steel.G_s}") + return (steel,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **Note**: Adding 3 arguments to the Material class raises an error. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.2 Saving materials + + To save an already instantiated Material object, you need to use the following method. + """) + return + + +@app.cell +def _(steel): + steel.save_material() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.3 Available materials + + Saved Materials are stored in a **.toml file**, which can be read as .txt. The file is placed on ROSS root file with name `available_materials.toml`. + + It's possible to access the Material data from the file. With the file opened, you can: + - modify the properties directly; + - create new materials; + + It's important to **keep the file structure** to ensure the correct functioning of the class. + + ``` + [Materials.Steel] + name = "Steel" + rho = 7810 + E = 211000000000.0 + Poisson = 0.2992610837438423 + G_s = 81200000000.0 + color = "#525252" + ``` + + **Do not change the dictionary keys and the order they're built**. + + To check what materials are available, use the command: + """) + return + + +@app.cell +def _(rs): + rs.Material.available_materials() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.4 Loading materials + + After checking the available materials, you should use the `Material.use_material('name')` method with the **name of the material** as a parameter. + """) + return + + +@app.cell +def _(rs): + steel5 = rs.Material.load_material("Steel") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 2: ShaftElement Class + + `ShaftElement` allows you to create cylindrical and conical shaft elements. It means you can set differents outer and inner diameters for each element node. + + There are some ways in which you can choose the parameters to model this element: + + - Euler–Bernoulli beam Theory (`rotary_inertia=False, shear_effects=False`) + - Timoshenko beam Theory (`rotary_inertia=True, shear_effects=True` - used as default) + + The matrices (mass, stiffness, damping and gyroscopic) will be defined considering the following local coordinate vector: + + $[x_0, y_0, \alpha_0, \beta_0, x_1, y_1, \alpha_1, \beta_1]^T$ + Where + $\alpha_0$ and $\alpha_1$ are the bending on the yz plane + $\beta_0$ and $\beta_1$ are the bending on the xz plane. + + + This element represents the rotor's shaft, all the other elements are correlated with this one. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 2.1 Creating shaft elements + + The next examples present different ways of how to create a ShaftElement object, from a single element to a list of several shaft elements with different properties. + + When creating shaft elements, you don't necessarily need to input a specific node. If `n=None`, the `Rotor` class will assign a value to the element when building a rotor model (*see section 6*). + + You can also pass the same `n` value to several shaft elements in the same rotor model. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.1.1 Cylindrical shaft element + + As it's been seen, a shaft element has 4 parameters for diameters. To simplify that, when creating a cylindrical element, you only need to give 2 of them: `idl` and `odl`. So the other 2 (`idr` and `odr`) get the same values. + + **Note**: you can give all the 4 parameters, as long they match each other. + """) + return + + +@app.cell +def _(rs, steel): + # Cylindrical shaft element + _L = 0.25 + _i_d = 0 + _o_d = 0.05 + cy_elem = rs.ShaftElement(L=_L, idl=_i_d, odl=_o_d, material=steel, shear_effects=True, rotary_inertia=True, gyroscopic=True) + print(cy_elem) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.1.2 Conical shaft element + + To create conical shaft elements, you must give all the 4 diameter parameters, and `idl != idr` and/or `odl != odr`. + """) + return + + +@app.cell +def _(rs, steel): + # Conical shaft element + _L = 0.25 + idl = 0 + idr = 0 + odl = 0.05 + odr = 0.07 + co_elem = rs.ShaftElement(L=_L, idl=idl, idr=idr, odl=odl, odr=odr, material=steel, shear_effects=True, rotary_inertia=True, gyroscopic=True) + print(co_elem) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Returning element matrices + + Use one of this methods to return the matrices: + - `.M()`: returns the mass matrix + - `.K(frequency)`: returns the stiffness matrix + - `.C(frequency)`: returns the damping matrix + - `.G()`: returns the gyroscopic matrix + """) + return + + +@app.cell +def _(): + # Mass matrix + # cy_elem.M() + + # Stiffness matrix + # frequency = 0 + # cy_elem.K(frequency) + + # Damping matrix + # frequency = 0 + # cy_elem.C(frequency) + + # Gyroscopic matrix + # cy_elem.G() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.1.3 List of elements - identical properties + + Now we learned how to create elements, let's automate the process of creating multiple elements with identical properties. + + In this example, we want 6 shaft elements with identical properties. This process can be done using a `for` loop or a list comprehension. + """) + return + + +@app.cell +def _(rs, steel): + # Creating a list of shaft elements + _L = 0.25 + _i_d = 0 + _o_d = 0.05 + _N = 6 + shaft_elements = [rs.ShaftElement(L=_L, idl=_i_d, odl=_o_d, material=steel, shear_effects=True, rotary_inertia=True, gyroscopic=True) for _ in range(_N)] + shaft_elements + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.1.4 List of elements - different properties + + Now we learned how to create elements, let's automate the process of creating multiple elements with different properties. + + In this example, we want 6 shaft elements which properties may not be the same. This process can be done using a `for` loop or a list comprehension, coupled with Python's `zip()` method. + + We create lists for each property, where each term refers to a single element: + """) + return + + +@app.cell +def _(rs, steel): + _L = [0.2, 0.2, 0.1, 0.1, 0.2, 0.2] + _i_d = [0.01, 0, 0, 0, 0, 0.01] + _o_d = [0.05, 0.05, 0.06, 0.06, 0.05, 0.05] + shaft_elements_1 = [rs.ShaftElement(L=l, idl=idl, odl=odl, material=steel, shear_effects=True, rotary_inertia=True, gyroscopic=True) for (l, idl, odl) in zip(_L, _i_d, _o_d)] + shaft_elements_1 + return + + +@app.cell +def _(rs, steel): + _L = [0.2, 0.2, 0.1, 0.1, 0.2, 0.2] + _i_d = [0.01, 0, 0, 0, 0, 0.01] + _o_d = [0.05, 0.05, 0.06, 0.06, 0.05, 0.05] + _N = len(_L) + shaft_elements_2 = [rs.ShaftElement(L=_L[i], idl=_i_d[i], odl=_o_d[i], material=steel, shear_effects=True, rotary_inertia=True, gyroscopic=True) for i in range(_N)] + shaft_elements_2 + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 2.2 Creating shaft elements via Excel + + There is an option for creating a list of shaft elements via an Excel file. The classmethod `.from_table()` reads an Excel file created and converts it to a list of shaft elements. + + A header with the names of the columns is required. These names should match the names expected by the routine (usually the names of the parameters, but also similar ones). The program will read every row bellow the header until they end or it reaches a NaN, which means if the code reaches to an empty line, it stops iterating. + + An example of Excel content can be found at ROSS GitHub repository at *ross/tests/data/shaft_si.xls*, spreadsheet "Model". + + You can load it using the following code. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ```python + shaft_file = Path("shaft_si.xls") + shaft = rs.ShaftElement.from_table( + file=shaft_file, sheet_type="Model", sheet_name="Model" + ) + ``` + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 2.3 Creating coupling element (CouplingElement Class) + + Here, we introduce the `CouplingElement` class, a subclass of the `ShaftElement` class, designed to model the interaction between two rotor shafts. This element is implemented in a general form in ROSS. The coupling plays a crucial role in mechanical systems by transmitting forces, vibrations, and motion between rotating shafts. It is primarily characterized by adding stiffness, mass, and inertia to the system, which are essential for simulating the real-world behavior of coupled rotors. + + __Steps:__ + - Create two shafts: Start by defining two rotor shafts using the `ShaftElement` class. Each shaft can have its own properties like material, length, and diameter. + - Join shafts with coupling: Use the `CouplingElement` class to connect the two shafts by inserting the elements in a list following a certain order. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.3.1 Define the shafts with their respective dimensions and properties + """) + return + + +@app.cell +def _(rs, steel): + # Shaft 1 - Left + L1 = [300, 92, 200, 200, 92, 300] # Segment lengths (in mm) + r1 = [61.5, 75, 75, 75, 75, 61.5] # Segment radii (in mm) + shaft1 = [ + rs.ShaftElement( + L=L1[i] * 1e-3, # Convert length to meters + idl=0.0, # Inner diameter in meters + odl=r1[i] * 2e-3, # Outer diameter in meters + material=steel, # Material used for the shaft + shear_effects=True, + rotary_inertia=True, + gyroscopic=True, + ) + for i in range(len(L1)) + ] + return (shaft1,) + + +@app.cell +def _(rs, steel): + # Shaft 2 - Right + L2 = [80, 200, 200, 640] # Segment lengths (in mm) + r2 = [160.5, 160.5, 130.5, 130.5] # Segment radii (in mm) + shaft2 = [ + rs.ShaftElement( + L=L2[i] * 1e-3, # Convert length to meters + idl=0.0, # Inner diameter in meters + odl=r2[i] * 2e-3, # Outer diameter in meters + material=steel, # Material used for the shaft + shear_effects=True, + rotary_inertia=True, + gyroscopic=True, + ) + for i in range(len(L2)) + ] + return (shaft2,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.3.2 Define the coupling element that connects the two shafts + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + In this example, only the torsional stiffness is adopted. However, you can pass any of the following arguments (if not provided, the default value is zero): + - kt_x, + - kt_y, + - kt_z (axial stiffness), + - kr_x, + - kr_y, + - kr_z (torsional stiffness), + + considering that the stiffness matrix $\mathbf{K}$ will be assembled like this: + + \begin{equation} + \mathbf{K} = + \begin{bmatrix} + k_{t_x} & 0 & 0 & 0 & 0 & 0 & -k_{t_x} & 0 & 0 & 0 & 0 & 0 \\ + 0 & k_{t_y} & 0 & 0 & 0 & 0 & 0 & -k_{t_y} & 0 & 0 & 0 & 0 \\ + 0 & 0 & k_{t_z} & 0 & 0 & 0 & 0 & 0 & -k_{t_z} & 0 & 0 & 0 \\ + 0 & 0 & 0 & k_{r_x} & 0 & 0 & 0 & 0 & 0 & -k_{r_x} & 0 & 0 \\ + 0 & 0 & 0 & 0 & k_{r_y} & 0 & 0 & 0 & 0 & 0 & -k_{r_y} & 0 \\ + 0 & 0 & 0 & 0 & 0 & k_{r_z} & 0 & 0 & 0 & 0 & 0 & -k_{r_z} \\ + -k_{t_x} & 0 & 0 & 0 & 0 & 0 & k_{t_x} & 0 & 0 & 0 & 0 & 0 \\ + 0 & -k_{t_y} & 0 & 0 & 0 & 0 & 0 & k_{t_y} & 0 & 0 & 0 & 0 \\ + 0 & 0 & -k_{t_z} & 0 & 0 & 0 & 0 & 0 & k_{t_z} & 0 & 0 & 0 \\ + 0 & 0 & 0 & -k_{r_x} & 0 & 0 & 0 & 0 & 0 & k_{r_x} & 0 & 0 \\ + 0 & 0 & 0 & 0 & -k_{r_y} & 0 & 0 & 0 & 0 & 0 & k_{r_y} & 0 \\ + 0 & 0 & 0 & 0 & 0 & -k_{r_z} & 0 & 0 & 0 & 0 & 0 & k_{r_z} + \end{bmatrix} + \end{equation} + + __Note:__ Although not demonstrated here, the same logic used for stiffness coefficients can also be applied to damping coefficients (ct_x, ct_y, ct_z, cr_x, cr_y, cr_z). + """) + return + + +@app.cell +def _(rs): + # Mass at each station (e.g., hub mass of the coupling) + mass_left_station = 151 / 2 + mass_right_station = 151 / 2 + + # Polar moment of inertia of the coupling + Ip = 3.48 + + # Torsional stiffness + torsional_stiffness = 3.04e6 + + # Create the coupling element + coupling = rs.CouplingElement( + m_l=mass_left_station, # Mass on the left station (in kg) + m_r=mass_right_station, # Mass on the right station (in kg) + Ip_l=Ip + / 2, # Polar moment of inertia on the left station (assuming equal distribution in kg·m²) + Ip_r=Ip + / 2, # Polar moment of inertia on the right station (assuming equal distribution in kg·m²) + kr_z=torsional_stiffness, # Torsional stiffness (in N·m/rad) + ) + coupling + return (coupling,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.3.3 Coupling all elements + + Combine all elements into a single list for the rotor model. + The elements must be in order. + """) + return + + +@app.cell +def _(coupling, shaft1, shaft2): + shaft_elements_3 = [*shaft1, coupling, *shaft2] + shaft_elements_3 + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 3: DiskElement Class + + The class `DiskElement` allows you to create disk elements, representing rotor equipments which can be considered only to add mass and inertia to the system, disregarding the stiffness. + + ROSS offers 3 (three) ways to create a disk element: + 1. Inputing mass and inertia data + 2. Inputing geometrical and material data + 3. From Excel table + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 3.1 Creating disk elements from inertia properties + + If you have access to the mass and inertia properties of a equipment, you can input the data directly to the element. + + Disk elements are useful to represent equipments which mass and inertia are significant, but the stiffness can be neglected. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 3.1.1 Creating a single disk element + + This example below shows how to instantiate a disk element according to the mass and inertia properties: + """) + return + + +@app.cell +def _(rs): + disk = rs.DiskElement(n=0, m=32.58, Ip=0.178, Id=0.329, tag="Disk") + disk + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 3.1.2 Creating a list of disk element + + This example below shows how to create a list of disk element according to the mass and inertia properties. The logic is the same applied to shaft elements. + """) + return + + +@app.cell +def _(rs): + # OPTION No.1: + # Using zip() method + _n_list = [2, 4] + _m_list = [32.6, 35.8] + _Id_list = [0.17808928, 0.17808928] + _Ip_list = [0.32956362, 0.38372842] + _disk_elements = [rs.DiskElement(n=_n, m=m, Id=Id, Ip=Ip) for (_n, m, Id, Ip) in zip(_n_list, _m_list, _Id_list, _Ip_list)] + _disk_elements + return + + +@app.cell +def _(rs): + # OPTION No.2: + # Using list index + _n_list = [2, 4] + _m_list = [32.6, 35.8] + _Id_list = [0.17808928, 0.17808928] + _Ip_list = [0.32956362, 0.38372842] + _N = len(_n_list) + _disk_elements = [rs.DiskElement(n=_n_list[i], m=_m_list[i], Id=_Id_list[i], Ip=_Ip_list[i]) for i in range(_N)] + _disk_elements + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 3.2 Creating disk elements from geometrical properties + + Besides the instantiation previously explained, there is a way to instantiate a DiskElement with only geometrical parameters (an approximation for cylindrical disks) and the disk’s material, as we can see in the following code. In this case, there's a class method (`rs.DiskElement.from_geometry()`) which you can use. + + ROSS will take geometrical parameters (outer and inner diameters, and width) and convert them into mass and inertia data. Once again, considering the disk as a cylinder. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 3.2.1 Creating a single disk element + + This example below shows how to instantiate a disk element according to the geometrical and material properties: + """) + return + + +@app.cell +def _(rs, steel): + _disk1 = rs.DiskElement.from_geometry(n=4, material=steel, width=0.07, i_d=0.05, o_d=0.28) + print(_disk1) + print('=' * 76) + print(f'Disk mass: {_disk1.m}') + print(f'Disk polar inertia: {_disk1.Ip}') + print(f'Disk diametral inertia: {_disk1.Id}') + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 3.2.2 Creating a list of disk element + + This example below shows how to create a list of disk element according to the geometrical and material properties. The logic is the same applied to shaft elements. + """) + return + + +@app.cell +def _(rs, steel): + # OPTION No.1: + # Using zip() method + _n_list = [2, 4] + _width_list = [0.7, 0.7] + _i_d_list = [0.05, 0.05] + _o_d_list = [0.15, 0.18] + _disk_elements = [rs.DiskElement.from_geometry(n=_n, material=steel, width=width, i_d=_i_d, o_d=_o_d) for (_n, width, _i_d, _o_d) in zip(_n_list, _width_list, _i_d_list, _o_d_list)] + _disk_elements + return + + +@app.cell +def _(rs, steel): + # OPTION No.2: + # Using list index + _n_list = [2, 4] + _width_list = [0.7, 0.7] + _i_d_list = [0.05, 0.05] + _o_d_list = [0.15, 0.18] + _N = len(_n_list) + _disk_elements = [rs.DiskElement.from_geometry(n=_n_list[i], material=steel, width=_width_list[i], i_d=_i_d_list[i], o_d=_o_d_list[i]) for i in range(_N)] + _disk_elements + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 3.3 Creating disk elements via Excel + + The third option for creating disk elements is via an Excel file. The classmethod `.from_table()` reads an Excel file created and converts it to a list of disk elements. This method accepts **only mass and inertia** inputs. + + A header with the names of the columns is required. These names should match the names expected by the routine (usually the names of the parameters, but also similar ones). The program will read every row bellow the header until they end or it reaches a NaN, which means if the code reaches to an empty line, it stops iterating. + + You can take advantage of the excel file used to assemble shaft elements, to assemble disk elements, just add a new spreadsheet to your Excel file and specify the correct `sheet_name`. + + An example of Excel content can be found at diretory *ross/tests/data/shaft_si.xls*, spreadsheet "More". + """) + return + + +@app.cell +def _(Path, rs): + _file_path = Path('shaft_si.xls') + _list_of_disks = rs.DiskElement.from_table(file=_file_path, sheet_name='More') + _list_of_disks + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 4: Bearing and Seal Classes + + ROSS has a series of classes to represent elements that add stiffness and / or damping to a rotor system. + They're suitable to represent mainly bearings, supports and seals. Each one aims to represent some types of bearing and seal. + + All the classes will return four stiffness coefficients ($k_{xx}$, $k_{xy}$, $k_{yx}$, $k_{yy}$) and four damping coefficients ($c_{xx}$, $c_{xy}$, $c_{yx}$, $c_{yy}$), which will be used to assemble the stiffness and damping matrices. + + The main difference between these classes are the arguments the user must input to create the element. + + Available bearing classes and class methods: + + - 1. `BearingElement`: represents a general (journal) bearing element. + - 2. `SealElement`: represents a general seal element. + - 3. `BallBearingElement`: A bearing element for ball bearings + - 4. `RollerBearingElement`: A bearing element for roller bearings. + - 5. `MagneticBearingElement`: A bearing element for magnetic bearings. + - 5.1. `param_to_coef`: A bearing element for magnetic bearings from electromagnetic parameters + - 6. `PlainJournal`: A cylindrical bearing element based on the pressure and temperature field in oil film. + + The classes from item 2 to 6 inherit from `BearingElement` class. It means, you can use the same methods and commands, set up to `BearingElement`, in the other classes. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 4.1 BearingElement Class + + This class will create a bearing element. Bearings are elements that only add stiffness and damping properties to the rotor system. These parameters are defined by 8 dynamic coefficients (4 stiffness coefficients and 4 damping coefficients). + + Parameters can be a constant value or speed dependent. For speed dependent parameters, each argument should be passed as an array and the correspondent speed values should also be + passed as an array. Values for each parameter will be interpolated for the speed. + + Bearing elements are single node elements and linked to "ground", but it's possible to create a new node with `n_link` argument to introduce a link with other elements. Useful to add bearings in series or co-axial rotors. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 4.1.1 Bearing with constant coefficients + + Bearings can have a constant value for each coefficient. In this case, it's **not necessary** to give a value to `frequency` argument. + + The next example shows how to instantiate a **single bearing with constant coefficients**: + """) + return + + +@app.cell +def _(rs): + _stfx = 1000000.0 + _stfy = 800000.0 + _bearing1 = rs.BearingElement(n=0, kxx=_stfx, kyy=_stfy, cxx=1000.0) + print(_bearing1) + print('=' * 55) + print(f'Kxx coefficient: {_bearing1.kxx}') + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 4.1.2 Bearing with varying coefficients + + The coefficients could be an array with different values for different rotation speeds, in that case you only have to give a parameter 'frequency' which is a array with the same size as the coefficients array. + + The next example shows how to instantiate a **single bearing with speed dependent parameters**: + """) + return + + +@app.cell +def _(np, rs): + bearing2 = rs.BearingElement( + n=0, + kxx=np.array([0.5e6, 1.0e6, 2.5e6]), + kyy=np.array([1.5e6, 2.0e6, 3.5e6]), + cxx=np.array([0.5e3, 1.0e3, 1.5e3]), + frequency=np.array([0, 1000, 2000]), + ) + + print(bearing2) + print("=" * 79) + print(f"Kxx coefficient: {bearing2.kxx}") + return (bearing2,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + If the size of coefficient and frequency arrays do not match, an `ValueError` is raised + + The next example shows the instantiate of a **bearing with odd parameters**: + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ```python + bearing_odd = rs.BearingElement( # odd dimensions + n=0, + kxx=np.array([0.5e6, 1.0e6, 2.5e6]), + kyy=np.array([1.5e6, 2.0e6, 3.5e6]), + cxx=np.array([0.5e3, 1.0e3, 1.5e3]), + frequency=np.array([0, 1000, 2000, 3000]) + ) + ``` + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 4.1.3 Inserting bearing elements in series + + Bearing and seal elements are 1-node element, which means the element attaches to a given node from the rotor shaft and it's connect to the "ground". However, there's an option to couple multiple elements in series, using the `n_link` argument. This is very useful to simulate structures which support the machine, for example. + + `n_link` opens a new node to the rotor system, or it can be associated to another rotor node (useful in co-axial rotor models). Then, the new BearingElement node, is set equal to the `n_link` from the previous element. + """) + return + + +@app.cell +def _(rs): + _stfx = 1000000.0 + _stfy = 800000.0 + bearing3 = rs.BearingElement(n=0, kxx=_stfx, kyy=_stfy, cxx=1000.0, n_link=1, tag='journal_bearing') + bearing4 = rs.BearingElement(n=1, kxx=10000000.0, kyy=1000000000.0, cxx=10, tag='support') + print(bearing3) + print(bearing4) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 4.1.4 Visualizing coefficients graphically + + If you want to visualize how the coefficients varies with speed, you can select a specific coefficient and use the `.plot()` method. + + Let's return to the example done in **4.1.2** and check how $k_{yy}$ and $c_{yy}$ varies. You can check for all the 8 dynamic coefficients as you like. + """) + return + + +@app.cell +def _(bearing2): + bearing2.plot("kyy") + return + + +@app.cell +def _(bearing2): + bearing2.plot(["kxx", "kyy"]) + return + + +@app.cell +def _(bearing2): + bearing2.plot("cyy") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 4.2 SealElement Class + + `SealElement` class method have the exactly same arguments than `BearingElement`. The differences are found in some considerations when assembbling a full rotor model. For example, a SealElement won't generate reaction forces in a static analysis. So, even they are very similar when built, they have different roles in the model. + + Let's see an example: + """) + return + + +@app.cell +def _(rs): + _stfx = 1000000.0 + _stfy = 800000.0 + seal = rs.SealElement(n=0, kxx=_stfx, kyy=_stfy, cxx=1000.0, cyy=800.0) + seal + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 4.3 BallBearingElement Class + + This class will create a bearing element based on some geometric and constructive parameters of ball bearings. The main difference is that cross-coupling stiffness and damping are not modeled in this case. + + Let's see an example: + """) + return + + +@app.cell +def _(np, rs): + _n = 0 + n_balls = 8 + d_balls = 0.03 + _fs = 500.0 + _alpha = np.pi / 6 + _tag = 'ballbearing' + ballbearing = rs.BallBearingElement(n=_n, n_balls=n_balls, d_balls=d_balls, fs=_fs, alpha=_alpha, tag=_tag) + ballbearing + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 4.4 RollerBearingElement Class + + This class will create a bearing element based on some geometric and constructive parameters of roller bearings. The main difference is that cross-coupling stiffness and damping are not modeled in this case. + + Let's see an example: + """) + return + + +@app.cell +def _(np, rs): + _n = 0 + n_rollers = 8 + l_rollers = 0.03 + _fs = 500.0 + _alpha = np.pi / 6 + _tag = 'rollerbearing' + rollerbearing = rs.RollerBearingElement(n=_n, n_rollers=n_rollers, l_rollers=l_rollers, fs=_fs, alpha=_alpha, tag=_tag) + rollerbearing + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 4.5 MagneticBearingElement Class + + This class creates a magnetic bearing element. You can input electromagnetic parameters and PID gains. ROSS converts them to stiffness and damping coefficients. To do it, use the class `MagneticBearingElement()` + + See the following reference for the electromagnetic parameters g0, i0, ag, nw, alpha: + Book: Magnetic Bearings. Theory, Design, and Application to Rotating Machinery + Authors: Gerhard Schweitzer and Eric H. Maslen + Page: 84-95 + + From: "Magnetic Bearings. Theory, Design, and Application to Rotating Machinery" + Authors: Gerhard Schweitzer and Eric H. Maslen + Page: 354 + + Let's see an example: + """) + return + + +@app.cell +def _(rs): + _n = 0 + g0 = 0.001 + i0 = 1.0 + ag = 0.0001 + nw = 200 + _alpha = 0.392 + kp_pid = 1.0 + kd_pid = 1.0 + k_amp = 1.0 + k_sense = 1.0 + _tag = 'magneticbearing' + mbearing = rs.MagneticBearingElement(n=_n, g0=g0, i0=i0, ag=ag, nw=nw, alpha=_alpha, kp_pid=kp_pid, kd_pid=kd_pid, k_amp=k_amp, k_sense=k_sense, tag=_tag) + mbearing + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 4.6 PlainJournal Class + + This class computes the pressure and temperature fields within the oil film of a cylindrical bearing while also determining the stiffness and damping coefficients. These calculations are based on a wide range of inputs, including bearing geometry, operating conditions, fluid properties, turbulence modeling, and mesh discretization. + + __Class arguments:__ + - Bearing geometry: + Parameters like axial_length, journal_radius, radial_clearance, etc., define the physical characteristics. + It supports multiple geometries (circular, lobe, or elliptical). + + - Operational conditions: + The class accounts for the speed, load, and operating type (e.g., "flooded" or "starvation"). + + - Fluid properties: + Parameters for lubricant type, oil flow, injection pressure, etc., ensure accurate simulation of fluid dynamics under varying conditions. + + - Turbulence model: + Incorporates turbulence effects using Reynolds numbers and a turbulence scaling factor (delta_turb), which enhances accuracy at higher speeds. + + - Mesh discretization: + Allows detailed control over numerical simulations by adjusting the number of circumferential and axial elements. + + - Methodology: + Offers two methods (lund, perturbation) for dynamic coefficient calculation. + """) + return + + +@app.cell +def _(rs): + cybearing = rs.PlainJournal( + axial_length=0.263144, + journal_radius=0.2, + radial_clearance=1.95e-4, + elements_circumferential=11, + elements_axial=3, + n_pad=2, + pad_arc_length=176, + preload=0, + geometry="circular", + reference_temperature=50, + frequency=rs.Q_([900], "RPM"), + fxs_load=0, + fys_load=-112814.91, + groove_factor=[0.52, 0.48], + lubricant="ISOVG32", + n=3, + sommerfeld_type=2, + initial_guess=[0.1, -0.1], + method="perturbation", + operating_type="flooded", + injection_pressure=0, + oil_flow=37.86, + show_coef=False, + print_result=False, + print_progress=False, + print_time=False, + ) + cybearing + return (cybearing,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Plot pressure distribution + + You can choose the axial element on which you want to see the pressure distribution on the bearing. + """) + return + + +@app.cell +def _(cybearing): + # Show pressure distribution on bearing for the first axial element + cybearing.plot_pressure_distribution(axial_element_index=0) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 4.7 Creating bearing elements via Excel + + There's an option for creating bearing elements via an Excel file. The classmethod `.from_table()` reads an Excel file and converts it to a `BearingElement` instance. Differently from creating shaft or disk elements, this method creates only a single bearing element. To create a list of bearing elements, the user should open several spreadsheets in the Excel file and run a list comprehension loop appending each element to the list. + + A header with the names of the columns is required. These names should match the names expected by the routine (usually the names of the parameters, but also similar ones). The program will read every row below the header until they end or it reaches a NaN, which means if the code reaches an empty line, it stops iterating. + + ```text + n : int + The node in which the bearing will be located in the rotor. + file: str + Path to the file containing the bearing parameters. + sheet_name: int or str, optional + Position of the sheet in the file (starting from 0) or its name. If none is passed, it is + assumed to be the first sheet in the file. + ``` + + An example of Excel content can be found at directory *ross/tests/data/bearing_seal_si.xls*, spreadsheet "XLUserKCM". + """) + return + + +@app.cell +def _(Path, rs): + # single bearing element + _file_path = Path('bearing_seal_si.xls') + bearing = rs.BearingElement.from_table(n=0, file=_file_path) + bearing + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + As `.from_table()` creates only a single bearing, let's see an example how to create multiple elements without typing the same command line multiple times. + + - First, in the EXCEL file, create multiple spreadsheets. Each one must hold the bearing coefficients and frequency data. + + - Then, create a list holding the node numbers for each bearing (respecting the order of the spreadsheets from the EXCEL file). + + - Finally, create a loop which iterates over the the nodes list and the spreadsheet. + """) + return + + +@app.cell +def _(): + # list of bearing elements + + # nodes = list with the bearing elements nodes number + # file_path = Path("bearing_seal_si.xls") + # bearings = [rs.BearingElement.from_table(n, file_path, sheet_name=i) for i, n in enumerate(nodes)] + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 5: PointMass Class + + The `PointMass` class creates a point mass element. This element can be used to link other elements in the analysis. The mass provided can be different on the x and y direction (e.g. different support inertia for x and y directions). + + `PointMass` also keeps the mass, stiffness, damping and gyroscopic matrices sizes consistence. When adding 2 bearing elements in series, it opens a new node with new degrees of freedom (DoF) (*see section 4.1.3*) and expands the stiffness and damping matrices. For this reason, it's necessary to add mass values to those DoF to match the matrices sizes. + + If you input the argument `m`, the code automatically replicate the mass value for both directions "x" and "y". + + Let's see an example of creating point masses: + """) + return + + +@app.cell +def _(rs): + # inputting m + p0 = rs.PointMass(n=0, m=2) + p0.M() # returns de mass matrices for the element + return + + +@app.cell +def _(rs): + # inputting mx and my + p1 = rs.PointMass(n=0, mx=2, my=3) + p1.M() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 6: Rotor Class + + `Rotor` is the main class from ROSS. It takes as argument lists with all elements and assembles the mass, gyroscopic, damping and stiffness global matrices for the system. The object created has several methods that can be used to evaluate the dynamics of the model (they all start with the prefix `.run_`). + + To use this class, you must input all the already instantiated elements in a list format. + + If the shaft elements are not numbered, the class set a number for each one, according to the element's position in the list supplied to the rotor constructor. + + To assemble the matrices, the `Rotor` class takes the local DoF's index from each element (element method `.dof_mapping()`) and calculate the global index + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 6.1 Creating a rotor model + Let's create a simple rotor model with $1.5 m$ length with 6 identical shaft elements, 2 disks, 2 bearings in the shaft ends and a support linked to the first bearing. First, we create the elements, then we input them to the `Rotor` class. + """) + return + + +@app.cell +def _(np, rs, steel): + _n = 6 + shaft_elem = [rs.ShaftElement(L=0.25, idl=0.0, odl=0.05, material=steel, shear_effects=True, rotary_inertia=True, gyroscopic=True) for _ in range(_n)] + _disk0 = rs.DiskElement.from_geometry(n=2, material=steel, width=0.07, i_d=0.05, o_d=0.28) + _disk1 = rs.DiskElement.from_geometry(n=4, material=steel, width=0.07, i_d=0.05, o_d=0.28) + _disks = [_disk0, _disk1] + _stfx = 1000000.0 + _stfy = 800000.0 + _bearing0 = rs.BearingElement(0, kxx=_stfx, kyy=_stfy, cxx=0, n_link=7) + _bearing1 = rs.BearingElement(6, kxx=_stfx, kyy=_stfy, cxx=0) + bearing2_1 = rs.BearingElement(7, kxx=_stfx, kyy=_stfy, cxx=0) + _bearings = [_bearing0, _bearing1, bearing2_1] + pm0 = rs.PointMass(n=7, m=30) + pointmass = [pm0] + rotor1 = rs.Rotor(shaft_elem, _disks, _bearings, pointmass) + print('Rotor total mass = ', np.round(rotor1.m, 2)) + print('Rotor center of gravity =', np.round(rotor1.CG, 2)) + rotor1.plot_rotor() + return (rotor1,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 6.2 Adding new nodes + + You can add new nodes to the model based on a list of their positions by using the method `.add_nodes()`: + """) + return + + +@app.cell +def _(rotor1): + node_position = [0.42, 1.1] + rotor1_new = rotor1.add_nodes(node_position) + rotor1_new.plot_rotor() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 6.3 Creating a rotor from sections + + An alternative to build rotor models is dividing the rotor in sections. Each section gets the same number of shaft elements. + + There's an important difference in this class method when placing disks and bearings. The argument `n` will refer, not to the element node, but to the section node. So if your model has 3 sections with 4 elements each, there're 4 section nodes and 13 element nodes. + + Let's repeat the rotor model from the last example, but using `.from_section()` class method, without the support. + """) + return + + +@app.cell +def _(np, rs, steel): + _i_d = 0 + _o_d = 0.05 + i_ds_data = [0, 0, 0] + # inner diameter of each section + o_ds_data = [0.05, 0.05, 0.05] + # outer diameter of each section + leng_data = [0.5, 0.5, 0.5] + # length of each section + material_data = [steel, steel, steel] + _stfx = 1000000.0 + _stfy = 800000.0 + # material_data = steel + _bearing0 = rs.BearingElement(n=0, kxx=_stfx, kyy=_stfy, cxx=1000.0) + _bearing1 = rs.BearingElement(n=3, kxx=_stfx, kyy=_stfy, cxx=1000.0) + _bearings = [_bearing0, _bearing1] + # n = 0 refers to the section 0, first node + _disk0 = rs.DiskElement.from_geometry(n=1, material=steel, width=0.07, i_d=0.05, o_d=0.28) + _disk1 = rs.DiskElement.from_geometry(n=2, material=steel, width=0.07, i_d=0.05, o_d=0.28) + # n = 3 refers to the section 2, last node + _disks = [_disk0, _disk1] + rotor2 = rs.Rotor.from_section(brg_seal_data=_bearings, disk_data=_disks, idl_data=i_ds_data, leng_data=leng_data, odl_data=o_ds_data, nel_r=4, material_data=steel) + print('Rotor total mass = ', np.round(rotor2.m, 2)) + # n = 1 refers to the section 1, first node + print('Rotor center of gravity =', np.round(rotor2.CG, 2)) + # n = 2 refers to the section 2, first node + rotor2.plot_rotor() + return (rotor2,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 6.4 Visualizing the rotor model + + It is interesting to plot the rotor to check if the geometry checks with what you wanted to the model. Use the `.plot_rotor()` method to create a plot. + + `nodes` argument is useful when your model has lots of nodes and the visualization of nodes label may be confusing. Set an increment to the plot nodes label + + ROSS uses **PLOTLY** as main plotting library: + + With the Plotly, you can hover the mouse icon over the shaft, disk and point mass elements to check some of their parameters. + """) + return + + +@app.cell +def _(rotor1): + rotor1.plot_rotor() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Let's visualize another rotor example with **overlapping shaft elements**: + """) + return + + +@app.cell +def _(Path, rs): + shaft_file = Path('shaft_si.xls') + shaft = rs.ShaftElement.from_table(file=shaft_file, sheet_type='Model', sheet_name='Model') + _file_path = Path('shaft_si.xls') + _list_of_disks = rs.DiskElement.from_table(file=_file_path, sheet_name='More') + _bearing1 = rs.BearingElement.from_table(n=7, file='bearing_seal_si.xls') + bearing2_2 = rs.BearingElement.from_table(n=48, file='bearing_seal_si.xls') + _bearings = [_bearing1, bearing2_2] + rotor3 = rs.Rotor(shaft, _list_of_disks, _bearings) + return (rotor3,) + + +@app.cell +def _(rotor3): + node_increment = 5 + rotor3.plot_rotor(nodes=node_increment) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 6.5 Saving a rotor model + + You can save a rotor model using the method `.save()`. This method saves the each element type and the rotor object in different *.toml* files. + + You just need to input a name and the diretory, where it will be saved. If you don't input a file_path, the rotor model is saved inside the "ross" folder. + + To save the `rotor2` we can use: + """) + return + + +@app.cell +def _(rotor2): + rotor2.save("rotor2.toml") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 6.6 Loading a rotor model + + You can load a rotor model using the method `.load()`. This method loads a previously saved rotor model. + + You just need to input the file path to the method. + + Now, let's load the `rotor2` we saved before: + """) + return + + +@app.cell +def _(rotor2, rs): + rotor2_1 = rs.Rotor.load("rotor2.toml") + rotor2_1 == rotor2 + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 6.7 Using CouplingElement as lumped-element + + Shafts can be connected using the `CouplingElement` class, which can also function as a lumped-element model. + For instance, a motor can be represented using a `CouplingElement` by specifying its stiffness, mass, and polar moment of inertia. + + Here's an example: + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 6.7.1 Creating coupling element + """) + return + + +@app.cell +def _(rs): + ktc = 3042560.0 # Torsional stiffness + Ipc = 3.47888 + mc = 151.55 + coupling_1 = rs.CouplingElement(m_l=mc / 2, m_r=mc / 2, Ip_l=Ipc / 2, Ip_r=Ipc / 2, kr_z=ktc, L=0.46, o_d=0.1755, tag='Coupling') # This parameter is used for visualization purposes + return Ipc, coupling_1, mc + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 6.7.2 Creating motor as lumped-element + """) + return + + +@app.cell +def _(Ipc, mc, rs): + ktm = 9.13e6 + Ipm = 156 + mm = mc * Ipm / Ipc + + motor = rs.CouplingElement( + m_l=mm / 2, + m_r=mm / 2, + Ip_l=Ipm / 2, + Ip_r=Ipm / 2, + kr_z=ktm, + L=1, # This parameter is used for visualization purposes + tag="Motor", + ) + return (motor,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 6.7.3 Connecting coupling and motor to the end + + Since `CouplingElement` is a type of `ShaftElement`, it must be inserted into the appropriate position within the `shaft_elements` array. + """) + return + + +@app.cell +def _(coupling_1, motor, rs, steel): + length = [200.0, 200.0, 200.0, 100.0, 1190.0, 160.0, 160.0, 1190.0, 100.0, 200.0, 200.0, 240.0, 210.0] + out_diam_left = [160.0, 160.0, 200.0, 200.0, 298.0, 430.0, 430.0, 298.0, 298.0, 200.0, 160.0, 160.0, 140.0] + out_diam_right = [160.0, 160.0, 200.0, 298.0, 298.0, 430.0, 430.0, 298.0, 200.0, 200.0, 160.0, 160.0, 140.0] + shaft_elements_4 = [rs.ShaftElement(material=steel, L=_L * 0.001, odl=out_diam_left[_n] * 0.001, odr=out_diam_right[_n] * 0.001, idl=0.0, idr=0.0, alpha=10, beta=0.0001) for (_n, _L) in enumerate(length)] + shaft_elements_4.append(coupling_1) + shaft_elements_4.append(motor) + return (shaft_elements_4,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 6.7.6 Creating coupled rotor + """) + return + + +@app.cell +def _(rs, shaft_elements_4): + _disk_elements = [rs.DiskElement(n=6, m=2833.0, Ip=2797.0, Id=1551.0)] + support_1 = rs.BearingElement(n=1, kxx=271600000.0, kxy=-134900000.0, kyx=-862800000.0, kyy=1811000000.0, cxx=1399000.0, cxy=-2895000.0, cyx=-2887000.0, cyy=15720000.0) + support_2 = rs.BearingElement(n=11, kxx=294400000.0, kxy=-156200000.0, kyx=-954900000.0, kyy=2066000000.0, cxx=1475000.0, cxy=-3147000.0, cyx=-3137000.0, cyy=17380000.0) + bearing_elements = [support_1, support_2] + crotor = rs.Rotor(shaft_elements_4, _disk_elements, bearing_elements) + crotor.plot_rotor().show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 7: ROSS Units System + + ROSS uses an units system package called [Pint](https://pint.readthedocs.io/en/stable/). + + `Pint` defines, operates and manipulates **physical quantities**: the product of a numerical value and a unit of measurement. It allows arithmetic operations between them and conversions from and to different units. + + With `Pint`, it's possible to define units to every element type available in ROSS and manipulate the units when plotting graphs. ROSS takes the user-defined units and internally converts them to the International System (SI). + + **Important**: It's not possible to manipulate units for attributes from any class. Attributes' values are always returned converted to SI. **Only plot methods** are able to manipulate the output unit. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 7.1 Inserting units + + Working with `Pint` requires a specific syntax to assign an unit to an argument. + + First of all, it's necessary to import a function called `Q_` from `ross.units`. This function must be assigned to every variable that are desired to have units, followed by a *tuple* containing the magnitude and the unit (in string format). + + The example below shows how to create a material using `Pint`, and how it is returned to the user. + """) + return + + +@app.cell +def _(rs): + from ross.units import Q_ + + rho = Q_(487.56237, "lb/foot**3") # Imperial System + E = Q_(211.0e9, "N/m**2") # International System + G_s = Q_(81.2e9, "N/m**2") # International System + + steel4 = rs.Material(name="steel", rho=rho, E=E, G_s=G_s) + return Q_, steel4 + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **Note**: Taking a closer look to the output values, the material density is converted to the SI and it's returned this way to the user. + """) + return + + +@app.cell +def _(steel4): + print(steel4) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + The same syntax applies to elements instantiation, if units are desired. Besides, notice the output is displayed in SI units. + + #### Shaft Element using Pint + """) + return + + +@app.cell +def _(Q_, rs, steel): + _L = Q_(10, 'in') + _i_d = Q_(0.0, 'meter') + _o_d = Q_(0.05, 'meter') + elem_pint = rs.ShaftElement(L=_L, idl=_i_d, odl=_o_d, material=steel) + print(elem_pint) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Bearing Element using Pint + """) + return + + +@app.cell +def _(Q_, rs): + kxx = Q_(2.54e4, "N/in") + cxx = Q_(1e2, "N*s/m") + + brg_pint = rs.BearingElement(n=0, kxx=kxx, cxx=cxx) + print(brg_pint) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 7.2 Manipulating units for plotting + + The plot methods presents arguments to change the units for each axis. This kind of manipulation does not affect the resulting data stored. It only converts the data on the graphs. + + The arguments names follow a simple logic. It is the "axis name" underscore "units" (axisname_units). It should help the user to identify which axis to modify. For example: + + - frequency_units: + - "rad/s", "RPM", "Hz"... + - amplitude_units: + - "m", "mm", "in", "foot"... + - displacement_units: + - "m", "mm", "in", "foot"... + - rotor_length_units: + - "m", "mm", "in", "foot"... + - moment_units: + - "N/m", "lbf/foot"... + + It's not necessary to add units previously to each element or material to use `Pint` with plots. But keep in mind ROSS will considers results values in the SI units. + + **Note**: If you input data using the Imperial System, for example, without using Pint, ROSS will consider it's in SI if you try to manipulate the units when plotting. + + Let's run a simple example of manipulating units for plotting. + """) + return + + +@app.cell +def _(np, rotor3): + samples = 31 + speed_range = np.linspace(315, 1150, samples) + + campbell = rotor3.run_campbell(speed_range) + return (campbell,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Plotting with default options will bring graphs with SI units. X and Y axes representing the frequencies are set to `rad/s` + """) + return + + +@app.cell +def _(campbell): + campbell.plot() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Now, let's change the units to `RPM`. + + Just by adding `frequency_units="rpm"` to plot method, you'll change the plot units. + """) + return + + +@app.cell +def _(campbell): + campbell.plot(frequency_units="RPM") + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/tutorial_part_1.rst b/docs/user_guide/tutorial_part_1.rst new file mode 100644 index 000000000..c0fef7be8 --- /dev/null +++ b/docs/user_guide/tutorial_part_1.rst @@ -0,0 +1,5 @@ +Tutorial Part 1 +=============== + +.. marimo:: tutorial_part_1.py + :height: 700px diff --git a/docs/user_guide/tutorial_part_2_1.py b/docs/user_guide/tutorial_part_2_1.py new file mode 100644 index 000000000..bef424631 --- /dev/null +++ b/docs/user_guide/tutorial_part_2_1.py @@ -0,0 +1,477 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Tutorial - Static and Modal Analyzes + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + This is the second part of a basic tutorial on how to use ROSS (rotordynamics open-source software), a Python library for rotordynamic analysis. In this tutorial, you will learn how to run several rotordynamic analyzes with your **rotor model**. + + To get results, we always have to use one of the `.run_` methods available for a rotor object. These methods will return objects that store the analysis results and that also have plot methods available. These methods will use the plotly library to make graphs common to a rotordynamic analysis. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Rotor model + + First, let's recover the rotor model built in the previous tutorial. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + import plotly.graph_objects as go + + pio.renderers.default = "notebook" + return np, rs + + +@app.cell +def _(rs): + rotor3 = rs.compressor_example() + node_increment = 5 + rotor3.plot_rotor(nodes=node_increment) + return (rotor3,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Rotor Analyses + + In the last tutorial we have learnt how to create a rotor model with `Rotor` class. Now, we'll use the same class to run the simulation. There're some methods, most of them with the prefix `run_` you can use to run the rotordynamics analyses. + + For Most of the methods, you can use the command `.plot()` to display a graphical visualization of the results (e.g `run_campbel().plot()`, `run_modal().plot_mode_3d(mode)`). + + ROSS offers the following analyses: + - Static analysis + - Modal analysis + - Campbell Diagram + + + ### Plotly library + ROSS uses **Plotly** for plotting results. All the figures can be stored and manipulated following Plotly API. + + The following sections presents the results and how to return the Plotly Figures. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 1.1 Static Analysis + + This method runs the static analysis for the rotor. It calculate the static deformation due the gravity effects (shaft and disks weight). It also returns the bending moment and shearing force on each node, and you can return a free-body-diagram representation for the rotor, with the self weight, disks weight and reaction forces on bearings displayed. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.1.1 Running static analysis + + To run the simulation, use the `.run_static()` method. You can define a variable to store the results. + + Storing the results, it's possible to return the following arrays: + - `disk_forces_nodal` + - `disk_forces_tag` + - `bearing_forces_nodal` + - `bearing_forces_tag` + - `disp_y` + - `Vx` + - `Bm` + """) + return + + +@app.cell +def _(rotor3): + static = rotor3.run_static() + return (static,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Returning forces + + #### Disk forces + - `.disk_forces_nodal`: + Returns a dictionaty expliciting the node where the disk is located and the force value. + + - `.disk_forces_tag`: + Returns a dictionaty expliciting the the disk tag is located and the force value. + + #### Bearing forces + - `.bearing_forces_nodal`: + Returns a dictionaty expliciting the node where the bearing is located and the force value. + + - `.bearing_forces_tag`: + Returns a dictionaty expliciting the the bearing tag is located and the force value. + """) + return + + +@app.cell +def _(rotor3): + print("Disk forces - nodes") + print(rotor3.disk_forces_nodal) + print("") + print("Disk forces - tags") + print(rotor3.disk_forces_tag) + + print("") + print("Bearing forces - nodes") + print(rotor3.bearing_forces_nodal) + print("") + print("Bearing forces - tags") + print(rotor3.bearing_forces_tag) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Other attributes from static analysis + - `.Vx`: Shearing force array + - `.Bm`: Bending moment array + - `.deformation` Displacement in Y direction + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.1.2 Plotting results + + With results stored, you can use some methods to plot the results. Currently, there're four plots you can retrieve from static analysis: + + - `.plot_free_body_diagram()` + - `.plot_deformation()` + - `.plot_shearing_force()` + - `.plot_bending_moment()` + + + #### Plotting free-body-diagram + """) + return + + +@app.cell +def _(static): + static.plot_free_body_diagram() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Plotting deformation + """) + return + + +@app.cell +def _(static): + static.plot_deformation() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Plotting shearing force diagram + """) + return + + +@app.cell +def _(static): + static.plot_shearing_force() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Plotting bending moment diagram + """) + return + + +@app.cell +def _(static): + static.plot_bending_moment() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 1.2 Modal Analysis + + ROSS performs the modal analysis through method `run_modal()`. + This method calculates natural frequencies, damping ratios and mode shapes. + + You must select a speed, which will be used as excitation frequency to calculate the system's eigenvalues and eigenvectors, and the number of eigenvalues and eigenvectors to be calculated is an optional argument (`num_modes`). + + After running the modal analysis, it's possible to return the following attributes: + - eigenvalues (evalues); + - eigenvectors (evectors); + - damped natural frequencies (wd); + - undamped natural frequencies (wn); + - damping ratio (damping_ratio); + - logarithmic decrement (log_dec). + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.2.1 Running modal analysis + + To run the modal analysis, choose a speed to instantiate the method. For different speeds, change the the argument and run `run_modal()` once again. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Returning undamped natural frequencies + """) + return + + +@app.cell +def _(rotor3): + rotor_speed = 100.0 # rad/s + modal = rotor3.run_modal(rotor_speed, num_modes=16) + print(f"Undamped natural frequencies:\n {modal.wn}") + return (modal,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Returning damped natural frequencies + """) + return + + +@app.cell +def _(modal): + # modal.wd + print(f"Damped natural frequencies:\n {modal.wd}") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Returning the damping ratio + """) + return + + +@app.cell +def _(modal): + # modal.damping_ratio + print(f"Damping ratio for each mode:\n {modal.damping_ratio}") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Returning logarithmic decrement + """) + return + + +@app.cell +def _(modal): + # modal.log_dec + print(f"Logarithmic decrement for each mode:\n {modal.log_dec}") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.2.2 Plotting results + + Once `run_modal()` is completed, you can check for the rotor's mode shapes. You can plot each one of the modes calculated. + + Besides, there're two options for visualization: + - `plot_mode_2d` - plotting 2D view + - `plot_mode_3d` - plotting 3D view + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Plotting 3D view + + Use the command `.plot_mode_3d(mode)`. + """) + return + + +@app.cell +def _(modal): + mode = 5 + modal.plot_mode_3d(mode) + return (mode,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Plotting 2D view + + Use the command `.plot_mode_2d(mode)`. + """) + return + + +@app.cell +def _(modal, mode): + modal.plot_mode_2d(mode) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Torsional analysis + + You can filter modes by the mode type attribute ("Lateral", "Axial", "Torsional"), for example: + """) + return + + +@app.cell +def _(modal): + torsional_modes = [ + mode for mode, shape in enumerate(modal.shapes) if shape.mode_type == "Torsional" + ] + torsional_modes + return (torsional_modes,) + + +@app.cell +def _(modal, torsional_modes): + modal.plot_mode_3d(torsional_modes[0], animation=True) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 1.3 Campbell Diagram + + Also called "whirl speed map" in rotordynamics, ROSS calculate and plots the modes’ damped eigenvalues and the logarithmic decrement as a function of rotor speed. + + To run the Campbell Diagram, use the command `.run_campbell()`. The user must input an array of speeds, which will be iterated to calculate each point on the graph. + + This method returns the damped natural frequencies, logarithmic decrement and the whirl values (values indicating the whirl direction: backward or forward). + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.3.1 Running campbell diagram + + In this example the whirl speed map is calculated for a speed range from 0 to 1000 rad/s (~9550 RPM). + + Storing the results, it's possible to return the following arrays: + - `wd` + - `log_dec` + - `whirl_values` + + Each value in these arrays is calculated for each speed value in `speed_range` + """) + return + + +@app.cell +def _(np, rotor3): + samples = 31 + speed_range = np.linspace(315, 1150, samples) + + campbell = rotor3.run_campbell(speed_range) + return (campbell,) + + +@app.cell +def _(campbell): + # results for each frequency + _frequency_index = 0 + print(campbell.wd[:, _frequency_index]) + return + + +@app.cell +def _(campbell): + # results for each frequency + _frequency_index = 1 + print(campbell.log_dec[:, _frequency_index]) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.3.2 Plotting results + + Now that the results are stored, use `.plot()` method to display the Campbell Diagram plot. + + For the Campbell Diagram, you can plot more than one harmonic. As default, the plot display only the 1x speed option. + Input a list with more harmonics to display it at the graph. + """) + return + + +@app.cell +def _(campbell): + campbell.plot(harmonics=[0.5, 1]) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/tutorial_part_2_1.rst b/docs/user_guide/tutorial_part_2_1.rst new file mode 100644 index 000000000..a65fe7e31 --- /dev/null +++ b/docs/user_guide/tutorial_part_2_1.rst @@ -0,0 +1,5 @@ +Tutorial Part 2 1 +================= + +.. marimo:: tutorial_part_2_1.py + :height: 700px diff --git a/docs/user_guide/tutorial_part_2_2.py b/docs/user_guide/tutorial_part_2_2.py new file mode 100644 index 000000000..33ea51f42 --- /dev/null +++ b/docs/user_guide/tutorial_part_2_2.py @@ -0,0 +1,713 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + # Tutorial - Time and Frequency Analyzes + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + This is the third part of a basic tutorial on how to use ROSS (rotordynamics open-source software). In this tutorial, you will learn how to run time and frequency analyzes with your **rotor model**. + + To get results, we always have to use one of the `.run_` methods available for a rotor object. These methods will return objects that store the analysis results and that also have plot methods available. These methods will use the plotly library to make graphs common to a rotordynamic analysis. + + We can also use units when plotting results. For example, for a unbalance response plot we have the `amplitude_units` argument and we can choose between any length unit available in pint such as ‘meter’, ‘inch’, etc. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Rotor model + + Again, let's recover the rotor model built in the previous tutorial. + """) + return + + +@app.cell +def _(): + import ross as rs + from ross.units import Q_ + from ross.probe import Probe + import numpy as np + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + + pio.renderers.default = "notebook" + return Probe, Q_, np, rs + + +@app.cell +def _(rs): + rotor3 = rs.compressor_example() + rotor3.plot_rotor(nodes=5) + return (rotor3,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Rotor Analyses + + There're some methods, most of them with the prefix `run_` you can use to run the rotordynamics analyses. For Most of the methods, you can use the command `.plot()` to display a graphical visualization of the results (e.g `run_freq_response().plot()`). + + ROSS offers the following analyses: + - Frequency response + - Unbalance response + - Time response + - Undamped Critical Speed Map + + ### Plotly library + ROSS uses **Plotly** for plotting results. All the figures can be stored and manipulated following Plotly API. + + The following sections presents the results and how to return the Plotly Figures. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 1.1 Frequency Response + + ROSS' method to calculate the Frequency Response Function is `run_freq_response()`. This method returns the magnitude and phase in the frequency domain. The response is calculated for each node from the rotor model. + + When plotting the results, you can choose to plot: + - **amplitude vs frequency**: `plot_magnitude()` + - **phase vs frequency**: `plot_phase()` + - **polar plot of amplitude vs phase**: `plot_polar_bode()` + - **all**: `plot()` + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.1.1 Clustering points + + The number of solution points is an important parameter to determine the computational cost of the simulation. Besides the classical method, using `numpy.linspace`, which creates an evenly spaced array over a specified interval, ROSS offers an automatic method to create an `speed_range` array. + + The method `clustering_points` generates an automatic array to run frequency response analyses. The frequency points are calculated based on the damped natural frequencies and their respective damping ratios. The greater the damping ratio, the more spread the points are. If the damping ratio, for a given critical speed, is smaller than 0.005, it is redefined to be 0.005 (for this method only). + + The main goal of this feature is getting a more accurate amplitude value for the respective critical frequencies and nearby frequency points. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.1.2 Running frequency response + + To run the this analysis, use the command `run_freq_response()`. You can give a specific `speed_range` or let the program run with the default options. In this case, no arguments are needed to input. + + First, let's run an example with a "user-defined" `speed_range`. Setting an array to `speed_range` will disable all the frequency spacing parameters. + """) + return + + +@app.cell +def _(np, rotor3): + samples = 61 + speed_range = np.linspace(315, 1150, samples) # rads/s + results1 = rotor3.run_freq_response(speed_range=speed_range) + return (results1,) + + +@app.cell +def _(results1): + results1.speed_range.size + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Now, let's run an example using _clustering points_ array. + """) + return + + +@app.cell +def _(): + # results1_2 = rotor2.run_freq_response(cluster_points=True, num_points=5, num_modes=12) + return + + +@app.cell +def _(): + # results1_2.speed_range.size + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + In the next section we'll check the difference between both results. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.1.3 Plotting results - Bode Plot + + We can plot the frequency response selecting the input and output degree of freedom. + + - Input is the degree of freedom to be excited; + - Output is the degree of freedom to be observed. + + Each shaft node has 6 local degrees of freedom (dof) $[x, y, z, \alpha, \beta, \theta]$, and each degree of freedom has it own index: + - $x$ → index 0 + - $y$ → index 1 + - $z$ → index 2 + - $\alpha$ → index 3 + - $\beta$ → index 4 + - $\theta$ → index 5 + + To select a DoF to input and a DoF to the output, we have to use the following correlation: + + $global\_dof = node\_number \cdot dof\_per\_node + dof\_index$ + + For example: + node 26, global dof $y$: + """) + return + + +@app.cell +def _(results1, rotor3): + _node = 26 + global_dof = _node * rotor3.number_dof + 1 + plot = results1.plot(inp=global_dof, out=global_dof) + # converting the first plot yaxis to log scale + # plot = results1.plot(inp=global_dof, out=global_dof) + # plot.update_yaxes(type="log", row=1, col=1) + # plot + plot + return + + +@app.cell +def _(): + # plot1_2 = results1_2.plot(inp=global_dof, out=global_dof) + # plot1_2 + + # # converting the first plot yaxis to log scale + # plot1_2 = results1_2.plot(inp=global_dof, out=global_dof) + # plot1_2.update_yaxes(type="log", row=1, col=1) + # plot1_2 + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 1.2 Unbalance Response + + + ROSS' method to simulate the reponse to an unbalance is `run_unbalance_response()`. This method returns the unbalanced response in the frequency domain for a given magnitide and phase of the unbalance, the node where it's applied and a frequency range. + + ROSS takes the magnitude and phase and converts to a complex force array applied to the given node: + + $$ + force = \left(\begin{array}{cc} + F \cdot e^{j\delta}\\ + -jF \cdot e^{j\delta}\\ + 0\\ + 0 + \end{array}\right) + $$ + + where: + - $F$ is the unbalance magnitude; + - $\delta$ is the unbalance phase; + - $j$ is the complex number notation; + + When plotting the results, you can choose to plot the: + - Bode plot options for a single degree of freedom: + - amplitude vs frequency: `plot_magnitude()` + - phase vs frequency: `plot_phase()` + - polar plot of amplitude vs phase: `plot_polar_bode()` + - all: `plot()` + - Deflected shape plot options: + - deflected shape 2d: `plot_deflected_shape_2d()` + - deflected shape 3d: `plot_deflected_shape_3d()` + - bending moment: `plot_bending_moment()` + - all: `plot_deflected_shape()` + + `run_unbalance_response()` is also able to work with clustering points ( _see section 7.4.1_ ). + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.2.1 Running unbalance response + + To run the Unbalance Response, use the command `.unbalance_response()` + + In this following example, we can obtain the response for a given unbalance and its respective phase in a selected node. Notice that it's possible to add multiple unbalances instantiating node, magnitude and phase as lists. + + The method returns the force response array (complex values), the displacement magnitude (absolute value of the forced response) and the phase of the forced response. + + Let's run an example with 2 unbalances in phase, trying to excite the first and the third natural vibration mode. + ```text + Unbalance1: node = 29 + magnitude = 0.003 + phase = 0 + Unbalance2: node = 33 + magnitude = 0.002 + phase = 0 + ``` + """) + return + + +@app.cell +def _(np, rotor3): + n1 = 29 + m1 = 0.003 + _p1 = 0 + n2 = 33 + m2 = 0.002 + _p2 = 0 + frequency_range = np.linspace(315, 1150, 101) + results2 = rotor3.run_unbalance_response([n1, n2], [m1, m2], [_p1, _p2], frequency_range) + return (results2,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.2.2 Plotting results - Bode Plot + + To display the bode plot, use the command `.plot(probe)` + + Where `probe` is a list of Probe objects that allows you to choose not only the node where to observe the response, but also the orientation. + + Probe orientation equals 0° refers to `+X` direction (DoFX), and probe orientation equals 90° (or $\frac{\pi}{2} rad$) refers to `+Y` direction (DoFY). + + You can insert multiple probes at once. + """) + return + + +@app.cell +def _(Probe, Q_, results2): + # probe = Probe(probe_node, probe_orientation) + _probe1 = Probe(15, Q_(45, 'deg')) # node 15, orientation 45° + _probe2 = Probe(35, Q_(45, 'deg')) # node 35, orientation 45° + # converting the first plot yaxis to log scale + # plot2 = results2.plot(probe=[probe1, probe2], probe_units="rad") + # plot2.update_yaxes(type="log", row=1, col=1) + # plot2 + results2.plot(probe=[_probe1, _probe2]) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.2.3 Plotting results - Deflected shape + + To display the deflected shape configuration, use the command `.plot_deflected_shape()` + """) + return + + +@app.cell +def _(results2): + results2.plot_deflected_shape(speed=649) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 1.3 Time Response + + ROSS' method to calculate displacements due a force in time domain is `run_time_response()`. This function will take a rotor object and plot its time response given a force and a time array. + + There are two ways to obtain the numerical solution: + - one by solving the system of equations in state space, and + - the other by numerical integration using the Newmark method. + + The force input must be a matrix $M \times N$, where: + - $M$ is the size of the time array; + - $N$ is the rotor's number of DoFs (you can access this value via attribute `.ndof`). + + Each row from the matrix represents a node, and each column represents a time step. + + Time Response allows you to plot the response for: + - a list of probes + - an orbit for a given node (2d plot) + - all the nodes orbits (3d plot) + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.3.1 Running time response in state space + + To run the Time Response, use the command `.run_time_response()`. + + Building the force matrix is not trivial. We recommend creating a matrix of zeros using *numpy.zeros()* and then, adding terms to the matrix. + + #### 1.3.1.1 Considering harmonic force + In this examples, let's create an harmonic force on node 26, in $x$ and $y$ directions (remember index notation from Frequency Response (section 7.4.2). We'll plot results from 0 to 16 seconds of simulation. + """) + return + + +@app.cell +def _(np, rotor3): + time_samples = 1001 + t = np.linspace(0, 16, time_samples) + speed = 600.0 + _node = 26 + F = np.zeros((time_samples, rotor3.ndof)) + F[:, _node * rotor3.number_dof + 0] = 10 * np.cos(2 * t) + F[:, _node * rotor3.number_dof + 1] = 10 * np.sin(2 * t) + # component on direction x + # component on direction y + response3 = rotor3.run_time_response(speed, F, t) + return (response3,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 1.3.1.2 Plotting results + + There 3 (three) different options to plot the time response: + - `.plot_1d()`: plot time response for given probes. + - `.plot_2d()`: plot orbit of a selected node of a rotor system. + - `.plot_3d()`: plot orbits for each node on the rotor system in a 3D view. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Ploting time response for list of probes + """) + return + + +@app.cell +def _(Probe, Q_, response3): + _probe1 = Probe(3, 0) # node 3, orientation 0° (X dir.) + _probe2 = Probe(3, Q_(90, 'deg')) # node 3, orientation 90°(Y dir.) + response3.plot_1d(probe=[_probe1, _probe2]) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Ploting orbit response for a single node + """) + return + + +@app.cell +def _(response3): + response3.plot_2d(node=26) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Ploting orbit response for all nodes + """) + return + + +@app.cell +def _(response3): + response3.plot_3d() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 1.3.1.3 Considering coded force methods + + In this example, we consider two forces of excitation: + - Unbalance Force ($F_{unb}$): The method `.unbalance_force_over_time()` calculates this time-varying force and returns a force array of shape `(ndof, len(t))`. + + - Weight ($W$): The method `.gravitational_force()` returns a static force array of shape `(ndof,)`, which must be expanded to match the time dimension. + + - Force Combination: + $ + F(t) = F_{unb}(t) + W + $ + """) + return + + +@app.cell +def _(Probe, Q_, np, rotor3): + _dt = 0.001 + t_1 = np.arange(0, 16, _dt) + speed_1 = 600.0 + _node = 29 + _Funb = rotor3.unbalance_force_over_time(node=[_node], magnitude=[0.003], phase=[0], omega=speed_1, t=t_1) + W = rotor3.gravitational_force(direction='y') + F_1 = (_Funb + W[:, np.newaxis]).T + response3_2 = rotor3.run_time_response(speed_1, F_1, t_1) + response3_2.plot_dfft(probe=[Probe(_node, 0), Probe(_node, Q_(90, 'deg'))], frequency_units='rad/s') + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.3.2 Running time response with Newmark method + + To run the Time Response in this case, use the command `.run_time_response()` with the argument `method="newmark"`. + + One of the advantages of this method is that we can consider an array of rotor velocities as a function of time, rather than just a constant velocity. Additionally, we can pass a callable function as an argument that returns an array of forces to be added to the right-hand side of the system of equations. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 1.3.2.1 Considering speed variation + + To illustrate this, let’s now consider just an unbalance force applied at node 29, with a run-up to 600 rad/s over 3 seconds. + """) + return + + +@app.cell +def _(Probe, Q_, np, rotor3): + _dt = 0.001 + t_2 = np.arange(0, 3, _dt) + speed_2 = np.linspace(0, 600.0, len(t_2)) + _node = 29 + _Funb = rotor3.unbalance_force_over_time(node=[_node], magnitude=[0.003], phase=[0], omega=speed_2, t=t_2) + F_2 = _Funb.T + response3_n1 = rotor3.run_time_response(speed_2, F_2, t_2, method='newmark') + response3_n1.plot_1d(probe=[Probe(_node, 0), Probe(_node, Q_(90, 'deg'))]) + return F_2, speed_2, t_2 + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 1.3.2.3 Custom external forces with `add_to_RHS` + + The `add_to_RHS` argument allows the user to dynamically modify the external force array applied to the rotor during time integration. In addition to a predefined force array $F$, an additional contribution can be introduced at each time step through a custom function, for example `calc_force`. + + This function is called automatically at every time step of the simulation and receives the following arguments: + - **step**: Current step index in the simulation. + - **time_step**: Current simulation time. + - **disp_resp**: Displacement response at the current step (useful if the force depends on displacement). + - **velc_resp**: Velocity response at the current step. + - **accl_resp**: Acceleration response at the current step. + + The function must return the additional force array for that time step. This enables the implementation of forces that are nonlinear or state-dependent (e.g. feedback control, impact forces). + + + For example: + """) + return + + +@app.cell +def _(F_2, rotor3, speed_2, t_2): + def calc_force(step, time_step, disp_resp, velc_resp, accl_resp): + damping_force = -0.01 * velc_resp # Example: Add a damping-dependent force + return damping_force + response3_n2 = rotor3.run_time_response(speed_2, F_2, t_2, method='newmark', add_to_RHS=calc_force) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 1.4 Harmonic Balance Method (HBM) + The Harmonic Balance Method (HBM) enables the computation of steady-state periodic responses of rotor systems subjected to harmonic excitations. + Unlike traditional time-domain integration methods, HBM determines the system response directly in the frequency domain, providing an efficient and accurate approach for steady-state vibration analysis. + + To run the Harmonic Balance Method, use the command `.run_harmonic_balance_response()`. + + This routine computes the steady-state harmonic response and provides post-processing tools for: + - Frequency-domain analysis; + - Time-domain reconstruction. + + Compared to the conventional `.run_time_response()` approach, the execution time of `.run_harmonic_balance_response()` is significantly reduced, since it avoids numerical integration over time. This emphasizes the computational efficiency of the Harmonic Balance approach for steady-state analyses. + + The current implementation follows the methodology proposed by {cite}`cunha2025`. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.4.1 Running + + Consider a rotor system subjected to the following harmonic excitation forces: + + $$ + F_{x,29}(t) = A_1 \cos(\omega t + p_1) + A_2 \cos(2\omega t + p_2) + A_3 \cos(3\omega t + p_3), + $$ + + $$ + F_{y,29}(t) = A_1 \sin(\omega t + p_1) + A_2 \sin(2\omega t + p_2) + A_3 \sin(3\omega t + p_3), + $$ + + $$ + F_{x,33}(t) = m \, e \, \omega^2 \cos(\omega t), + $$ + + $$ + F_{y,33}(t) = m \, e \, \omega^2 \sin(\omega t). + $$ + + These expressions represent a combination of multi-harmonic excitations applied at node 29, and a classical unbalance excitation applied at node 33, where $\omega$ is rotational angular frequency (rad/s), $t$ is time (s), $A$ is force amplitude (N), $p$ is phase angle (rad), $m$ is unbalance mass (kg), and $e$ is eccentricity of the unbalance (m). + """) + return + + +@app.cell +def _(np, rotor3, rs): + speed_3 = 200.0 + t_3 = np.arange(0, 10, 0.0001) + (A1, A2, A3) = (1.0, 10.0, 5.0) + (_p1, _p2, p3) = (0.0, 0.0, 0.0) + (m, e) = (0.2, 0.01) + probe = rs.Probe(15, rs.Q_(45, 'deg')) + hb_results = rotor3.run_harmonic_balance_response(speed=speed_3, t=t_3, harmonic_forces=[{'node': 29, 'magnitudes': [A1, A2, A3], 'phases': [_p1, _p2, p3], 'harmonics': [1, 2, 3]}, {'node': 33, 'magnitudes': [m * e * speed_3 ** 2], 'phases': [0], 'harmonics': [1]}], n_harmonics=3) + return hb_results, probe + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.4.2 Plotting frequency-domain response + """) + return + + +@app.cell +def _(hb_results, probe): + hb_fig = hb_results.plot([probe], frequency_units="Hz") + hb_fig + return (hb_fig,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.4.3 Plotting deflected shape + """) + return + + +@app.cell +def _(hb_results): + hb_results.plot_deflected_shape() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.4.4 Time-domain reconstruction of the steady-state response + """) + return + + +@app.cell +def _(hb_results, probe): + hb_time_resp = hb_results.get_time_response() + hb_time_resp.plot_1d([probe]) + return (hb_time_resp,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.4.5 Comparison – Harmonic Balance vs. DFFT Spectrum + """) + return + + +@app.cell +def _(hb_fig, hb_time_resp, rs): + hb_time_resp.plot_dfft( + probe=[rs.Probe(15, rs.Q_(45, "deg"), tag="DFFT")], + frequency_units="Hz", + frequency_range=(0, 100), + yaxis_type="log", + fig=hb_fig, + ) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## 1.5 Undamped Critical Speed Map (UCS) + This method will plot the undamped critical speed map for a given range of stiffness values. If the range is not provided, the bearing stiffness at rated speed will be used to create a range. + + Whether a synchronous analysis is desired, the method selects only the foward modes and the frequency of the first forward mode will be equal to the speed. + + To run the UCS Map, use the command `.plot_ucs()`. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.5.1 Running and plotting UCS Map + + In this example the UCS Map is calculated for a stiffness range from **10E6** to **10E11** N/m. The other options are left to default. + """) + return + + +@app.cell +def _(rotor3): + stiff_range = (6, 11) + ucs_results = rotor3.run_ucs(stiffness_range=stiff_range, num=20, num_modes=16) + ucs_fig = ucs_results.plot() + ucs_fig + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/tutorial_part_2_2.rst b/docs/user_guide/tutorial_part_2_2.rst new file mode 100644 index 000000000..4d7e8a0e9 --- /dev/null +++ b/docs/user_guide/tutorial_part_2_2.rst @@ -0,0 +1,5 @@ +Tutorial Part 2 2 +================= + +.. marimo:: tutorial_part_2_2.py + :height: 700px diff --git a/docs/user_guide/tutorial_part_3.py b/docs/user_guide/tutorial_part_3.py new file mode 100644 index 000000000..672795d20 --- /dev/null +++ b/docs/user_guide/tutorial_part_3.py @@ -0,0 +1,949 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Tutorial - Stochastic ROSS + ====================== + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + This is a basic tutorial on how to use STOCHASTIC ROSS - a ROSS' module for stochastic rotordynamics analysis. Before starting this tutorial, be sure you're already familiar with ROSS library. + + If you've already used ROSS, you've noticed the graphs present deterministic results, considering a set of parameters. In other words, the model always produce the same output from a given starting condition or initial state. In STOCHASTIC ROSS, the concept is different, and we'll work it stochastic processes. + + A stochastic process is defined as a indexed collection of random variables defined on a common probability space ($\Omega$, $\mathcal{F}$, $P$} where $\Omega$ is a sample space, $\mathcal{F}$ is a $\sigma$-algebra, and $P$ is a probability measure. The index is often assumed to be time. + + This new module allows you to work with random variables applied to the ROSS' functions. Basically, any element or material can be receive a parameter considered random. Moreover, some methods are also able to receive a random variable (random force, random unbalance...). It means that a parameter, once assumed deterministic (int or float in python language), now follows a distribution (list or array), like uniform distribution, normal distribution, etc. + + As consequence, plots do not display deterministic results anymore. Instead, plots shows the expectation $E(X_t(t))$ (or mean) for a stochastic process and intervals of confidence (user choice). + + Where: + - $X_t$ is the stochastic process; + - $t$ is the index time + """) + return + + +@app.cell +def _(): + import ross as rs + import ross.stochastic as srs + from ross.probe import Probe + import numpy as np + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + import plotly.graph_objects as go + + pio.renderers.default = "notebook" + return Probe, np, rs, srs + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Random Sampling + + Arrays of random numbers can be creating using [`numpy.random`](https://docs.scipy.org/doc/numpy-1.14.0/reference/routines.random.html) package. + + `numpy.random` has a large set of distributions that cover most of our needs to run STOCHASTIC ROSS. + In this [LINK](https://docs.scipy.org/doc/numpy-1.14.0/reference/routines.random.html) you can find a list of numpy random numbers generators. + + When using STOCHASTIC ROSS, **all the randam variables must have the same size**. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Classes Name + + It's important to highlight that in STOCHASTIC ROSS, the classes name are the same than ROSS, but with a "**ST_**" prefix to differ. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## ST_Material + + There is a class called ST_Material to hold material's properties, where: + + `ST_Material` allows you to create a material with random properties. It creates an object containing a generator with random instances of [`rs.Material`](https://ross-rotordynamics.github.io/ross-website/generated/material/ross.Material.html#ross.Material). + + The instantiation is the same than `rs.Material` class. It has the same parameters and assumptions. The only difference is that you are able to select some parameters to consider as random and instantiate it as a list. + + The parameters which can be passed as random are: + - `rho` - Density + - `E` - Young's modulus + - `G_s` - Shear modulus + - `Poisson` - Poisson ratio + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ```text + name : str + Material name. + rho : float, list, pint.Quantity + Density (kg/m**3). + Input a list to make it random. + E : float, list, pint.Quantity + Young's modulus (N/m**2). + Input a list to make it random. + G_s : float, list + Shear modulus (N/m**2). + Input a list to make it random. + Poisson : float, list + Poisson ratio (dimensionless). + Input a list to make it random. + color : str + Can be used on plots. + ``` + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Note that, to instantiate a ST_Material class, you only need to give 2 out of the following parameters: `E`, `G_s` ,`Poisson`. + + Let's consider that the Young's Modulus is a random variable the follows a uniform distribution from $208e9$ to $211e9$ $N/m^2$. + """) + return + + +@app.cell +def _(np, srs): + var_size = 5 + _E = np.random.uniform(208000000000.0, 211000000000.0, var_size) + rand_mat = srs.ST_Material(name='Steel', rho=7810, E=_E, G_s=81200000000.0) + # Random values for Young's Modulus + print(rand_mat['E']) + return (rand_mat,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + You can return the random Materials created using the following command: + `iter()` + It returns a generator with the random objects. It consumes less computational memory and runs loops faster. + """) + return + + +@app.cell +def _(rand_mat): + list(iter(rand_mat)) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + You can pass one or all parameters as random (but remember the rule of given only 2 out of `E`, `G_s` ,`Poisson`). + + Let's see another example considering all parameters as random. + """) + return + + +@app.cell +def _(np, srs): + var_size_1 = 5 + _E = np.random.uniform(208000000000.0, 211000000000.0, var_size_1) + rho = np.random.uniform(7780, 7850, var_size_1) + G_s = np.random.uniform(79800000000.0, 81500000000.0, var_size_1) + rand_mat_1 = srs.ST_Material(name='Steel', rho=rho, E=_E, G_s=G_s) + list(iter(rand_mat_1)) + return (rand_mat_1,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## ST_ShaftElement + + `ST_ShaftElement` allows you to create random shaft element. It creates an object containing a generator with random instances of `ShaftElement`. + + The instantiation is the same than [`rs.ShaftElement`](https://ross-rotordynamics.github.io/ross-website/generated/elements/ross.ShaftElement.html#ross.ShaftElement) class. It has the same parameters and the same beam model and assumptions. The only difference is that you are able to select some parameters to consider as random and instantiate it as a list. + + The parameters which can be passed as random are: + - `L` - Length + - `idl` - Inner diameter of the element at the left position + - `odl` - Outer diameter of the element at the left position + - `idr` - Inner diameter of the element at the right position + - `odr` - Outer diameter of the element at the right position. + - `material` - Shaft material + + The selected parameters must be appended to `is_random` list as string. + + You can return the random shaft element created using the following command: + `iter()`. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ```text + L : float, pint.Quantity, list + Element length. + Input a list to make it random. + idl : float, pint.Quantity, list + Inner diameter of the element at the left position. + Input a list to make it random. + odl : float, pint.Quantity, list + Outer diameter of the element at the left position. + Input a list to make it random. + idr : float, pint.Quantity, list, optional + Inner diameter of the element at the right position + Default is equal to idl value (cylindrical element) + Input a list to make it random. + odr : float, pint.Quantity, list, optional + Outer diameter of the element at the right position. + Default is equal to odl value (cylindrical element) + Input a list to make it random. + material : ross.material, list of ross.material + Shaft material. + Input a list to make it random. + n : int, optional + Element number (coincident with it's first node). + If not given, it will be set when the rotor is assembled + according to the element's position in the list supplied to + shear_effects : bool, optional + Determine if shear effects are taken into account. + Default is True. + rotary_inertia : bool, optional + Determine if rotary_inertia effects are taken into account. + Default is True. + gyroscopic : bool, optional + Determine if gyroscopic effects are taken into account. + Default is True. + shear_method_calc : str, optional + Determines which shear calculation method the user will adopt. + Default is 'cowper' + is_random : list + List of the object attributes to become random. + Possibilities: + ["L", "idl", "odl", "idr", "odr", "material"] + ``` + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Cylindrical shaft element with random outer diameter + If you want to create a cylindrical element with random outer diameter, making sure both `odl` and `odr` are the same, input only `odl` parameter. + + The same logic is applied to inner diameter. + """) + return + + +@app.cell +def _(np, rand_mat_1, srs): + var_size_2 = 5 + _L = 0.25 + _i_d = 0.0 + _o_d = np.random.uniform(0.04, 0.06, var_size_2) + _is_random = ['odl', 'material'] + r_s0 = srs.ST_ShaftElement(L=_L, idl=_i_d, odl=_o_d, material=rand_mat_1, shear_effects=True, rotary_inertia=True, gyroscopic=True, is_random=_is_random) + list(iter(r_s0)) + return (r_s0,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Conical shaft element with random outer diameter + If you want to create a conical element with random outer diameter, input lists for `odl` ans `odr` parameters. + """) + return + + +@app.cell +def _(np, rand_mat_1, srs): + var_size_3 = 5 + _L = 0.25 + idl = 0.0 + idr = 0.0 + odl = np.random.uniform(0.04, 0.06, var_size_3) + odr = np.random.uniform(0.06, 0.07, var_size_3) + _is_random = ['odl', 'odr', 'material'] + r_s1 = srs.ST_ShaftElement(L=_L, idl=idl, odl=odl, idr=idr, odr=odr, material=rand_mat_1, shear_effects=True, rotary_inertia=True, gyroscopic=True, is_random=_is_random) + list(iter(r_s1)) + return (var_size_3,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Creating a list of shaft elements + + Let's see 2 examples of building rotor shafts: + - a shaft with 5 shaft elements considered random + ``` + shaft_elements = [ + ST_ShaftElement, + ST_ShaftElement, + ST_ShaftElement, + ST_ShaftElement, + ST_ShaftElement, + ] + ``` + - a shaft with 5 elements, being only the 3rd element considered as random. So we want; + ``` + shaft_elements = [ + ShaftElement, + ShaftElement, + ST_ShaftElement, + ShaftElement, + ShaftElement, + ] + ``` + + First we create the deterministic shaft elements. + """) + return + + +@app.cell +def _(np, srs, var_size_3): + from ross.materials import steel + _L = 0.25 + _N = 5 + _l_list = [_L for _ in range(_N)] + shaft_elements = [srs.ST_ShaftElement(L=l, idl=0.0, odl=np.random.uniform(0.04, 0.06, var_size_3), material=steel, shear_effects=True, rotary_inertia=True, gyroscopic=True, is_random=['odl']) for l in _l_list] + for i in range(_N): + print('Element', i) + print(list(iter(shaft_elements[i]))) + return (steel,) + + +@app.cell +def _(r_s0, rs, steel): + _L = 0.25 + _i_d = 0.0 + _o_d = 0.05 + _N = 4 + _l_list = [_L for _ in range(_N)] + shaft_elements_1 = [rs.ShaftElement(L=l, idl=_i_d, odl=_o_d, material=steel, shear_effects=True, rotary_inertia=True, gyroscopic=True) for l in _l_list] + list(iter(shaft_elements_1)) + shaft_elements_1.insert(2, r_s0) + list(iter(shaft_elements_1)) + return (shaft_elements_1,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## ST_DiskElement + + This class represents a random Disk element. + + `ST_DiskElement` allows you to create random disk element. It creates an object containing a generator with random instances of [`rs.DiskElement`](https://ross-rotordynamics.github.io/ross-website/generated/elements/ross.DiskElement.html#ross.DiskElement). + + The instantiation is the same than `DiskElement` class. It has the same parameters and assumptions. The only difference is that you are able to select some parameters to consider as random and instantiate it as a list. + + The parameters which can be passed as random are: + - `m` - mass + - `Id` - Diametral moment of inertia. + - `Ip` - Polar moment of inertia + + The selected parameters must be appended to `is_random` list as string. + + You can return the random disk element created using the following command: + `iter()`. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ```text + n: int + Node in which the disk will be inserted. + m : float, list + Mass of the disk element. + Input a list to make it random. + Id : float, list + Diametral moment of inertia. + Input a list to make it random. + Ip : float, list + Polar moment of inertia + Input a list to make it random. + tag : str, optional + A tag to name the element + Default is None + color : str, optional + A color to be used when the element is represented. + Default is '#b2182b' (Cardinal). + is_random : list + List of the object attributes to become random. + Possibilities: + ["m", "Id", "Ip"] + ``` + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + All the values are following the S.I. convention for the units. + """) + return + + +@app.cell +def _(np, srs, var_size_3): + _m = np.random.uniform(32.0, 33.0, var_size_3) + Id = np.random.uniform(0.17, 0.18, var_size_3) + Ip = np.random.uniform(0.32, 0.33, var_size_3) + _is_random = ['m', 'Id', 'Ip'] + disk0 = srs.ST_DiskElement(n=2, m=_m, Id=Id, Ip=Ip, is_random=_is_random) + list(iter(disk0)) + return (disk0,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### From geometry DiskElement instantiation + + Besides the instantiation previously explained, there is a way to instantiate a ST_DiskElement with only geometrical parameters (for cylindrical disks) and the disk’s material, as we can see in the following code. + + Use the classmethod `ST_DiskElement.from_geometry`. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ```text + n: int + Node in which the disk will be inserted. + material: ross.Material, list of ross.Material + Disk material. + Input a list to make it random. + width: float, list + The disk width. + Input a list to make it random. + i_d: float, list + Inner diameter. + Input a list to make it random. + o_d: float, list + Outer diameter. + Input a list to make it random. + tag : str, optional + A tag to name the element + Default is None + is_random : list + List of the object attributes to become random. + Possibilities: + ["material", "width", "i_d", "o_d"] + ``` + """) + return + + +@app.cell +def _(np, srs, steel, var_size_3): + _i_d = np.random.uniform(0.05, 0.06, var_size_3) + _o_d = np.random.uniform(0.35, 0.39, var_size_3) + disk1 = srs.ST_DiskElement.from_geometry(n=3, material=steel, width=0.07, i_d=_i_d, o_d=_o_d, is_random=['i_d', 'o_d']) + list(iter(disk1)) + return (disk1,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## ST_BearingElement + + This class represents a random bearing element. + + `ST_BearingElement` allows you to create random disk element. It creates an object containing a generator with random instances of [`rs.BearingElement`](https://ross-rotordynamics.github.io/ross-website/generated/elements/ross.BearingElement.html#ross.BearingElement). + + The instantiation is the same than `BearingElement` class. It has the same parameters and assumptions. The only difference is that you are able to select some parameters to consider as random and instantiate it as a list. + + If you're considering constant coefficients, use an 1-D array to make it random. + If you're considering varying coefficients to the frequency, use a 2-D array to make it random + + The parameters which can be passed as random are: + - `kxx` - Direct stiffness in the x direction. + - `cxx` - Direct damping in the x direction. + - `kyy` - Direct stiffness in the y direction. + - `cyy` - Direct damping in the y direction. + - `kxy` - Cross coupled stiffness in the x direction. + - `cxy` - Cross coupled damping in the x direction. + - `kyx` - Cross coupled stiffness in the y direction. + - `cyx` - Cross coupled damping in the y direction. + + The selected parameters must be appended to `is_random` list as string. + + You can return the random disk element created using the following command: + `iter()`. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ```text + n: int + Node which the bearing will be located in + kxx: float, 1-D array, 2-D array + Direct stiffness in the x direction. + cxx: float, 1-D array, 2-D array + Direct damping in the x direction. + kyy: float, 1-D array, 2-D array, optional + Direct stiffness in the y direction. + (defaults to kxx) + kxy: float, 1-D array, 2-D array, optional + Cross coupled stiffness in the x direction. + (defaults to 0) + kyx: float, 1-D array, 2-D array, optional + Cross coupled stiffness in the y direction. + (defaults to 0) + cyy: float, 1-D array, 2-D array, optional + Direct damping in the y direction. + (defaults to cxx) + cxy: float, 1-D array, 2-D array, optional + Cross coupled damping in the x direction. + (defaults to 0) + cyx: float, 1-D array, 2-D array, optional + Cross coupled damping in the y direction. + (defaults to 0) + frequency: array, optional + Array with the frequencies (rad/s). + tag: str, optional + A tag to name the element + Default is None. + n_link: int, optional + Node to which the bearing will connect. If None the bearing is + connected to ground. + Default is None. + scale_factor: float, optional + The scale factor is used to scale the bearing drawing. + Default is 1. + is_random : list + List of the object attributes to become stochastic. + Possibilities: + ["kxx", "kxy", "kyx", "kyy", "cxx", "cxy", "cyx", "cyy"] + ``` + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Bearing with random constant values for each coefficient: + """) + return + + +@app.cell +def _(np, srs): + var_size_4 = 5 + _kxx = np.random.uniform(1000000.0, 2000000.0, var_size_4) + _cxx = np.random.uniform(1000.0, 2000.0, var_size_4) + brg0 = srs.ST_BearingElement(n=0, kxx=_kxx, cxx=_cxx, is_random=['kxx', 'cxx']) + brg1 = srs.ST_BearingElement(n=5, kxx=_kxx, cxx=_cxx, is_random=['kxx', 'cxx']) + list(iter(brg0)) + return brg0, brg1, var_size_4 + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + The coefficients could be an array with different values for different rotation speeds, in that case you only have to give a parameter 'frequency' which is a array with the same size as the coefficients array. + + To make it random, check the example below: + """) + return + + +@app.cell +def _(np, srs, var_size_4): + _kxx = [np.random.uniform(1000000.0, 2000000.0, var_size_4), np.random.uniform(2300000.0, 3300000.0, var_size_4)] + _cxx = [np.random.uniform(1000.0, 2000.0, var_size_4), np.random.uniform(2100.0, 3100.0, var_size_4)] + frequency = np.linspace(500, 800, len(_kxx)) + brg2 = srs.ST_BearingElement(n=1, kxx=_kxx, cxx=_cxx, frequency=frequency, is_random=['kxx', 'cxx']) + list(iter(brg2)) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## ST_Rotor + + This class will create several instances of [`rs.Rotor`](https://ross-rotordynamics.github.io/ross-website/generated/results/ross.Rotor.html#ross.Rotor) class. The number of rotors to be created depends on the amount of random elements instantiated and theirs respective sizes. + + To use this class, you only have to give all the already instantiated elements in a list format, as it follows. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ```text + shaft_elements : list + List with the shaft elements + disk_elements : list + List with the disk elements + bearing_seal_elements : list + List with the bearing elements + point_mass_elements: list + List with the point mass elements + tag : str + A tag for the rotor + ``` + """) + return + + +@app.cell +def _(brg0, brg1, disk0, disk1, shaft_elements_1, srs): + rotor1 = srs.ST_Rotor(shaft_elements_1, [disk0, disk1], [brg0, brg1]) + return (rotor1,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Running the simulation + + After you verify that everything is fine with the rotor, you should + run the simulation and obtain results. + To do that you only need to use the one of the `.run_()` methods available. + + For now, STOCHASTIC ROSS has only a few stochastic analysis as shown below. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Obtaining results + + These are the following stochastic analysis you can do with the program: + + - `.run_campbell()` - Campbell Diagram + - `.run_freq_response()` - Frequency response + - `.run_unbalance_response()` - Unbalance response + - `.run_time_response()` - Time response + + ## Plotting results + + As it has been spoken before, STOCHASTIC ROSS presents results, not deterministic as ROSS does, but in the form of expectation (mean values) and percentiles (or confidence intervals). When plotting these analysis, it will always display the expectation and you are able to choose which percentile to plot. + + To return a plot, you need to enter the command `.plot()` rigth before the command the run an analysis: + `.run_something().plot()` + + `.plot()` methods have two main arguments: + ```text + percentile : list, optional + Sequence of percentiles to compute, which must be between + 0 and 100 inclusive. + conf_interval : list, optional + Sequence of confidence intervals to compute, which must be between + 0 and 100 inclusive. + ``` + + ### Plot interaction + + You can click on the legend label to ommit an object from the graph. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Campbell Diagram + + This function will calculate the damped natural frequencies for a speed range. + + ```text + speed_range : array + Array with the desired range of frequencies. + frequencies : int, optional + Number of frequencies that will be calculated. + Default is 6. + frequency_type : str, optional + Choose between displaying results related to the undamped natural + frequencies ("wn") or damped natural frequencies ("wd"). + The default is "wd". + ``` + + To run the Campbell Diagram, use the command `.run_campbell()` + + To plot the figure, use `.run_campbell().plot()` + + Notice that there're two plots. You can plot both or one of them: + - damped natural frequency vs frequency; + - use `.run_campbell().plot_nat_freq()` + - log dec vs frequency + - use `.run_campbell().plot_log_dec()` + """) + return + + +@app.cell +def _(np, rotor1): + samples = 31 + _speed_range = np.linspace(0, 500, samples) + camp = rotor1.run_campbell(_speed_range, frequencies=7) + return (camp,) + + +@app.cell +def _(camp): + fig1 = camp.plot_nat_freq(conf_interval=[90]) + fig1.show(renderer="notebook") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Frenquency Response + + ```text + speed_range : array + Array with the desired range of frequencies. + inp : int + Degree of freedom to be excited. + out : int + Degree of freedom to be observed. + modes : list, optional + Modes that will be used to calculate the frequency response + (all modes will be used if a list is not given). + ``` + + We can put the frequency response of selecting the input and output degree of freedom. + - Input is the degree of freedom to be excited; + - Output is the degree of freedom to be observed. + + Each shaft node has 6 local degrees of freedom (dof) $[x, y, z, \alpha, \beta, \theta]$, and each degree of freedom has it own index: + - $x$ → index 0 + - $y$ → index 1 + - $z$ → index 2 + - $\alpha$ → index 3 + - $\beta$ → index 4 + - $\theta$ → index 5 + + Taking the rotor built as example, let's excite the node 3 (in the $y$ direction) and observe the response on the node 2 (also in $y$ direction): + + $global\_dof = node\_number * dof\_per\_node + dof\_index$ + + node 2, local dof $y$: + + $out = 2 * 6 + 1 = 13$ + + node 3, local dof $y$: + + $inp = 3 * 6 + 1 = 19$ + + To run the Frequency Response, use the command `.run_freq_response()` + + To plot the figure, use the command `run_freq_response().plot()` + """) + return + + +@app.cell +def _(np, rotor1): + _speed_range = np.linspace(0, 500, 301) + inp = 3 * rotor1.number_dof + 1 + out = 2 * rotor1.number_dof + 1 + freqresp = rotor1.run_freq_response(inp, out, _speed_range) + return (freqresp,) + + +@app.cell +def _(freqresp): + fig2 = freqresp.plot(conf_interval=[90], mag_kwargs=dict(yaxis=dict(type="log"))) + fig2.show(renderer="notebook") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Unbalance Response + + This method returns the unbalanced response for a mdof system given magnitide and phase of the unbalance, the node where it's applied and a frequency range. + + ```text + node : list, int + Node where the unbalance is applied. + magnitude : list, float + Unbalance magnitude. + If node is int, input a list to make make it random. + If node is list, input a list of lists to make it random. + phase : list, float + Unbalance phase. + If node is int, input a list to make make it random. + If node is list, input a list of lists to make it random. + frequency_range : list, float + Array with the desired range of frequencies. + ``` + + In this analysis, you can enter **magnitude** and **phase** as random variables. + + To run the Unbalance Response, use the command `.run_unbalance_response()` + + To plot the figure, use the command `.run_unbalance_response().plot(probe)` + + Where `probe` is a list of tuples that allows you to choose not only the node where to observe the response, but also the orientation. + + Probe orientation equals 0º refers to `+X` direction (DoFX), and probe orientation equals 90º (or $\frac{\pi}{2} rad$) refers to `+Y` direction (DoFY). + + In this following example, we can obtain the response for a random unbalance(kg.m) with a uniform distribution and its respective phase in a selected node. Notice that it's possible to add multiple unbalances instantiating node, magnitude and phase as lists. + + ```text + Unbalance: node = 3 + magnitude = np.random.uniform(0.001, 0.002, 10) + phase = 0 + ``` + """) + return + + +@app.cell +def _(np, rotor1): + freq_range = np.linspace(0, 500, 201) + n = 3 + _m = np.random.uniform(0.001, 0.002, 10) + p = 0.0 + results = rotor1.run_unbalance_response(n, _m, p, freq_range) + return (results,) + + +@app.cell +def _(Probe, np, results): + fig3 = results.plot( + probe=[Probe(3, np.pi / 2)], + conf_interval=[90], + mag_kwargs=dict(yaxis=dict(type="log")), + ) + fig3.show(renderer="notebook") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### Time Response + + This function will take a rotor object and plot its time response given a force and a time. + The **force** and **ic** parameters can be passed as random. + + This function takes the following parameters: + ```text + speed: float + Rotor speed + force : 2-dimensional array, 3-dimensional array + Force array (needs to have the same number of rows as time array). + Each column corresponds to a dof and each row to a time step. + Inputing a 3-dimensional array, the method considers the force as + a random variable. The 3rd dimension must have the same size than + ST_Rotor.rotor_list + time_range : 1-dimensional array + Time array. + ic : 1-dimensional array, 2-dimensional array, optional + The initial conditions on the state vector (zero by default). + Inputing a 2-dimensional array, the method considers the + initial condition as a random variable. + ``` + + To run the Time Response, use the command `.run_time_response()` + + To plot the figure, use the command `.run_time_response().plot()` + + In the following example, let's apply harmonic forces to the node 3 in both directions $x$ and $y$. Also lets analyze the first 10 seconds from the response for a speed of 100.0 rad/s (~955.0 RPM). + """) + return + + +@app.cell +def _(np, rotor1): + size = 1000 + ndof = rotor1.ndof + node = 3 # node where the force is applied + speed = 100.0 + t = np.linspace(0, 10, size) + F = np.zeros((size, ndof)) + F[:, node * rotor1.number_dof + 0] = 10 * np.cos(2 * t) + F[:, node * rotor1.number_dof + 1] = 10 * np.sin(2 * t) + results_1 = rotor1.run_time_response(speed, F, t) + return node, results_1 + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Plotting Time Response 1D, 2D and 3D + """) + return + + +@app.cell +def _(Probe, np, results_1): + fig4 = results_1.plot_1d(probe=[Probe(3, np.pi / 2)], conf_interval=[90]) + fig4.show(renderer='notebook') + return + + +@app.cell +def _(node, results_1): + fig5 = results_1.plot_2d(node=node, conf_interval=[90]) + fig5.show(renderer='notebook') + return + + +@app.cell +def _(results_1): + fig6 = results_1.plot_3d(conf_interval=[90]) + fig6.show(renderer='notebook') + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/tutorial_part_3.rst b/docs/user_guide/tutorial_part_3.rst new file mode 100644 index 000000000..4e0655a27 --- /dev/null +++ b/docs/user_guide/tutorial_part_3.rst @@ -0,0 +1,5 @@ +Tutorial Part 3 +=============== + +.. marimo:: tutorial_part_3.py + :height: 700px diff --git a/docs/user_guide/tutorial_part_4.py b/docs/user_guide/tutorial_part_4.py new file mode 100644 index 000000000..1f3f8af7c --- /dev/null +++ b/docs/user_guide/tutorial_part_4.py @@ -0,0 +1,477 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Tutorial - MultiRotor System + ====================== + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + This is a basic tutorial on how to use the `MultiRotor` class for rotordynamics analysis. Before starting this tutorial, be sure you're already familiar with ROSS library. What changes here is basically the part of building the model, since running the analyses will be practically the same as seen in the other tutorials. + + When two shafts are joined together by gears, coupling can occur between the lateral and torsional vibration. This interaction is modeled in ROSS based on {cite}`rao1998theoretical`. In this work, a typical spur gear pair is modeled as a pair of rigid disks connected by a spring and a damping, considering the pressure angle ($\alpha$) and the oritentation angle ($\varphi$) as shown in the following: + +
+ Gear mesh +
+ Figure 1: Global coordinate system of a spur gear pair (Yang et al., 2016). +
+ + As you can see, an element not yet shown is needed to model the multi-rotor system, the `GearElement`. This new element resembles the `DiskElement` with added attributes, which will be shown next. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 1: GearElement Class + + The class `GearElement` allows you to create gear elements. It is a subclass of `DiskElement` that requires information related to its pitch diameter and pressure angle. + + ROSS offers 2 (two) ways to create a gear element: + 1. Inputing mass and inertia data + 2. Inputing geometrical and material data + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 1.1 Creating a single gear element + + In this tutorial, only an example of how to create a single element will be shown. However, the same procedures presented for the `DiskElement` in the first part of the Tutorial can be replicated here. The `GearElement` goes into the same list of disk elements when assembling the rotor. + + This example below shows how to instantiate a gear element according to the mass and inertia properties: + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + import pandas as pd + + from ross.units import Q_ + + # Make sure the default renderer is set to 'notebook' for inline plots in Jupyter + import plotly.io as pio + import plotly.graph_objects as go + + pio.renderers.default = "notebook" + return Q_, np, pd, rs + + +@app.cell +def _(Q_, rs): + gear = rs.GearElement( + n=0, + m=5, + Id=0.002, + Ip=0.004, + n_teeth=20, + pitch_diameter=0.5, + pr_angle=Q_(22.5, "deg"), + tag="Gear", + ) + gear + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 2: MultiRotor Class + + `MultiRotor` is a subclass of `Rotor` class. It takes two rotors (driving and driven) as arguments and couple them with their gears. The object created has several methods that can be used to evaluate the dynamics of the model (they all start with the prefix `.run_`). + + To use this class, you must input the already instantiated rotors and each one need at least one gear element. + + The shaft elements are renumbered starting with the elements of the driving rotor. + + To assemble the matrices, the driving and driven matrices are joined. + For the stiffness matrix, the coupling is considered at the nodes of the gears in contact. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 2.1 Creating a spur geared two-shaft rotor system + Let's create a simple model with two rotors connected by a pair of spur gears and supported by flexible bearings, as shown in Fig. 2. For more details on the description of the model, see the work of {cite}`rao1998theoretical`. + +
+ MultiRotor +
+ Figure 2: Rotors connected by a pair of spur gears (Friswell et al., 2010). +
+ + In Figure 2, the first node is 1 (one), but we must remember that in ROSS the node count starts at 0 (zero). + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.1.1 Creating material + """) + return + + +@app.cell +def _(rs): + # Creating material + material = rs.Material(name="mat_steel", rho=7800, E=207e9, G_s=79.5e9) + return (material,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.1.2 Creating the driving rotor + """) + return + + +@app.cell +def _(Q_, material, rs): + # Rotor 1 + _L1 = [0.1, 4.24, 1.16, 0.3] + d1 = [0.3, 0.3, 0.22, 0.22] + _shaft1 = [rs.ShaftElement(L=_L1[i], idl=0.0, odl=d1[i], material=material, shear_effects=True, rotary_inertia=True, gyroscopic=True) for i in range(len(_L1))] + generator = rs.DiskElement(n=1, m=525.7, Id=16.1, Ip=32.2) + disk = rs.DiskElement(n=2, m=116.04, Id=3.115, Ip=6.23) + gear1 = rs.GearElement(n=4, m=726.4, Id=56.95, Ip=113.9, n_teeth=328, base_diameter=0.5086 * 2, pr_angle=Q_(22.5, 'deg'), helix_angle=0) + bearing1 = rs.BearingElement(n=0, kxx=183900000.0, kyy=200400000.0, cxx=3000.0) + bearing2 = rs.BearingElement(n=3, kxx=183900000.0, kyy=200400000.0, cxx=3000.0) + rotor1 = rs.Rotor(_shaft1, [generator, disk, gear1], [bearing1, bearing2]) + rotor1.plot_rotor() + return (rotor1,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.1.3 Creating the driven rotor + """) + return + + +@app.cell +def _(material, np, rs): + # Rotor 2 + _L2 = [0.3, 5, 0.1] + d2 = [0.15, 0.15, 0.15] + _shaft2 = [rs.ShaftElement(L=_L2[i], idl=0.0, odl=d2[i], material=material, shear_effects=True, rotary_inertia=True, gyroscopic=True) for i in range(len(_L2))] + base_radius = 0.03567 + _pressure_angle = rs.Q_(22.5, 'deg').to_base_units().m + pitch_diameter = 2 * base_radius / np.cos(_pressure_angle) + gear2 = rs.GearElement(n=0, m=5, Id=0.002, Ip=0.004, n_teeth=23, pitch_diameter=pitch_diameter, pr_angle=_pressure_angle, helix_angle=0) + turbine = rs.DiskElement(n=2, m=7.45, Id=0.0745, Ip=0.149) + bearing3 = rs.BearingElement(n=1, kxx=10100000.0, kyy=41600000.0, cxx=3000.0) + bearing4 = rs.BearingElement(n=3, kxx=10100000.0, kyy=41600000.0, cxx=3000.0) + rotor2 = rs.Rotor(_shaft2, [gear2, turbine], [bearing3, bearing4]) + rotor2.plot_rotor() + return (rotor2,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.1.4 Connecting rotors + + To build the multi-rotor model, we need to inform, in the following order: + - the driving rotor, + - the driven rotor, + - the tuple with the pair of coupled nodes (first number corresponds to the gear node of the driving rotor, and the second of the driven rotor), + - the gear ratio, and + - the gear mesh stiffness. + + Finally, we can inform: + - the orientation angle (if not defined, zero is adopted as the default), + - the position of the driven rotor in relation to the driving rotor only for visualization in the plot ("above" or "below"), and + - a tag. + """) + return + + +@app.cell +def _(rotor1, rotor2, rs): + # Creating multi-rotor + multirotor = rs.MultiRotor( + rotor1, + rotor2, + coupled_nodes=(4, 0), + gear_mesh_stiffness=1e8, + orientation_angle=0, + position="below", + ) + + multirotor.plot_rotor() + return (multirotor,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 2.2 Running analyses + We will run some analyses for the multi-rotor in this section and even compare results from the literature. + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.2.1 Modal analysis + + Let's start with the modal analysis to obtain the natural frequencies for the coupled rotor when the generator runs at + 1500 RPM. Then we will compare the results with {cite}`friswell2010dynamics`. + + It is worth noting that in the analyses, we must always inform the respective speed of the driving rotor and not the driven one. + """) + return + + +@app.cell +def _(Q_, multirotor, np, pd): + # Friswell et al. (2010) results for natural frequencies: + Friswell_results = np.array( + [ + 11.641, + 12.284, + 17.268, + 18.458, + 23.956, + 37.681, + 49.889, + 50.861, + 56.248, + 57.752, + 59.188, + 63.113, + 74.203, + ] + ) + + speed = Q_(1500, "RPM") + frequencies = 13 + + modal = multirotor.run_modal(speed, num_modes=frequencies * 2) + wd = np.round(Q_(modal.wd, "rad/s").to("Hz").m, 5) + + print("Natural frequencies (Hz)") + pd.DataFrame( + { + "Friswell et al.": Friswell_results, + "ROSS": wd, + "Error (%)": np.abs(wd - Friswell_results) / Friswell_results * 100, + } + ) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.2.2 Campbell diagram + + To obtain the Campbell diagram we can proceed in the same way as seen for a single rotor. Remember that the reference speeds / frequencies are in relation to the driving rotor. + + In the Campbell diagram below, the dashed lines show the shaft rotation speeds corresponding to the generator (blue, node 1) and turbine (yellow, node 7). + """) + return + + +@app.cell +def _(Q_, multirotor, np): + frequency_range = Q_(np.arange(0, 5000, 100), "RPM") + + gear_ratio = multirotor.mesh.gear_ratio + + campbell = multirotor.run_campbell(frequency_range, frequencies=13) + campbell.plot(frequency_units="Hz", harmonics=[1, round(gear_ratio, 3)]).show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### 2.2.3 Time response + + Using the `run_time_response` function we can obtain the system response due to an unbalance at nodes 2 and 7. The same setup presented in the work of {cite}`yang2016general` was applied here for comparison purposes. + """) + return + + +@app.cell +def _(Q_, multirotor, np): + nodes = [2, 7] + unb_mag = [35.505e-3, 0.449e-3] + unb_phase = [0, 0] + + dt = 1e-4 + t = np.arange(0, 1200, dt) + speed1 = Q_(5000, "RPM").to_base_units().m # Generator rotor speed + + # Unbalance force + F = multirotor.unbalance_force_over_time(nodes, unb_mag, unb_phase, speed1, t) + return F, speed1, t + + +@app.cell +def _(F, multirotor, speed1, t): + # Time response + time_resp = multirotor.run_time_response(speed1, F.T, t) + amp_resp = time_resp.yout + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Due to computational cost limitations, the entire analysis performed for comparison is not included in this tutorial. However, by running the `run_time_response` function across the complete range of speeds, it is possible to obtain the following responses at nodes 2 and 7 as shown in the figures below: + +
+ MultiRotor + MultiRotor +
+ Figure 3: Comparison of ROSS results with Yang et al. (2016) for the spur geared two-shaft rotor system. +
+ """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 2.3 Creating a spur geared multi-shaft rotor system + + Let's create a more complex model with three connected rotors. More details of this example can be found in {cite}`yang2016general`. + """) + return + + +@app.cell +def _(Q_, material, rs): + _L1 = [300, 92, 200, 200, 92, 300] + r1 = [61.5, 75, 75, 75, 75, 61.5] + _shaft1 = [rs.ShaftElement(L=_L1[i] * 0.001, idl=0.0, odl=r1[i] * 0.002, material=material, shear_effects=True, rotary_inertia=True, gyroscopic=True) for i in range(len(_L1))] + D1 = rs.DiskElement(n=0, m=66.63, Id=0.431, Ip=0.735) + D2 = rs.DiskElement(n=6, m=69.83, Id=0.542, Ip=0.884) + cxx = 3000.0 + B1 = rs.BearingElement(n=2, kxx=550000000.0, kyy=670000000.0, cxx=cxx) + B2 = rs.BearingElement(n=4, kxx=550000000.0, kyy=670000000.0, cxx=cxx) + _pressure_angle = Q_(22.5, 'deg') + G1 = rs.GearElement(n=3, m=14.37, Id=0.068, Ip=0.136, n_teeth=37, base_diameter=0.19, pr_angle=_pressure_angle) + rotor1_1 = rs.Rotor(_shaft1, [D1, D2, G1], [B1, B2]) + _L2 = [80, 200, 200, 640] + r2 = [160.5, 160.5, 130.5, 130.5] + _shaft2 = [rs.ShaftElement(L=_L2[i] * 0.001, idl=0.0, odl=r2[i] * 0.002, material=material, shear_effects=True, rotary_inertia=True, gyroscopic=True) for i in range(len(_L2))] + B3 = rs.BearingElement(n=1, kxx=3200000000.0, kyy=4600000000.0, cxx=cxx) + B4 = rs.BearingElement(n=3, kxx=3200000000.0, kyy=4600000000.0, cxx=cxx) + G2 = rs.GearElement(n=2, m=813.79, Id=52.36, Ip=104.72, n_teeth=244, base_diameter=1.23, pr_angle=_pressure_angle) + rotor2_1 = rs.Rotor(_shaft2, [G2], [B3, B4]) + L3 = [300, 110, 200, 200, 110, 300] + r3 = [64.5, 76.5, 76.5, 76.5, 76.5, 64.5] + shaft3 = [rs.ShaftElement(L=L3[i] * 0.001, idl=0.0, odl=r3[i] * 0.002, material=material, shear_effects=True, rotary_inertia=True, gyroscopic=True) for i in range(len(_L1))] + D3 = rs.DiskElement(n=0, m=95.06, Id=1.097, Ip=1.532) + D4 = rs.DiskElement(n=6, m=96.22, Id=1.11, Ip=1.62) + G3 = rs.GearElement(n=3, m=19.52, Id=0.098, Ip=0.195, n_teeth=47, base_diameter=0.24, pr_angle=_pressure_angle) + B5 = rs.BearingElement(n=2, kxx=720000000.0, kyy=840000000.0, cxx=cxx) + B6 = rs.BearingElement(n=4, kxx=720000000.0, kyy=840000000.0, cxx=cxx) + rotor3 = rs.Rotor(shaft3, [D3, D4, G3], [B5, B6]) + return rotor1_1, rotor2_1, rotor3 + + +@app.cell +def _(Q_, rotor1_1, rotor2_1, rs): + # Connect rotor 1 with rotor 2 (driving rotor) + multi_rotor1 = rs.MultiRotor(rotor2_1, rotor1_1, coupled_nodes=(2, 3), gear_mesh_stiffness=255000000.0, orientation_angle=Q_(270, 'deg'), position='above') + return (multi_rotor1,) + + +@app.cell +def _(Q_, multi_rotor1, rotor3, rs): + # Connect rotor 3 with rotor 2 in multi rotor + psi = 90 + final_system = rs.MultiRotor( + multi_rotor1, + rotor3, + coupled_nodes=(2, 3), + gear_mesh_stiffness=2.6e8, + orientation_angle=Q_(270 - psi, "deg"), + position="below", + ) + return (final_system,) + + +@app.cell +def _(final_system): + final_system.plot_rotor() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + If we apply two unbalances with 92 g.mm in phase opposition at two ends of rotor 1 (nodes 5 and 11), while one unbalance with 294 g.mm at the middle of rotor 3 (node 15), we can obtain the following responses at node 15: + + +
+ MultiRotor +
+ Figure 4: Comparison of ROSS results with Yang et al. (2016) for the spur geared multi-shaft rotor system. +
+ """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + We can also vary the orientation angle between the rotors: + +
+ MultiRotor +
+ Figure 5: Unbalance responses at node 15 with three orientation angles. +
+ """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## References + + ```{bibliography} + :filter: docname in docnames + ``` + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/tutorial_part_4.rst b/docs/user_guide/tutorial_part_4.rst new file mode 100644 index 000000000..aca5b90bf --- /dev/null +++ b/docs/user_guide/tutorial_part_4.rst @@ -0,0 +1,5 @@ +Tutorial Part 4 +=============== + +.. marimo:: tutorial_part_4.py + :height: 700px diff --git a/docs/user_guide/tutorial_part_5.py b/docs/user_guide/tutorial_part_5.py new file mode 100644 index 000000000..f256915ef --- /dev/null +++ b/docs/user_guide/tutorial_part_5.py @@ -0,0 +1,637 @@ +import marimo + +__generated_with = "0.19.9" +app = marimo.App() + + +@app.cell +def _(): + import marimo as mo + + return (mo,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Tutorial - Active Magnetic Bearings + ====================== + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + This tutorial provides a comprehensive guide on how to model, simulate, and analyze Active Magnetic Bearings (AMBs) using ROSS. + + ## Section 1: Introduction and Working Principle + + **Active Magnetic Bearings (AMBs)** are support mechanisms that levitate a rotating shaft using electromagnetic forces, eliminating physical contact between the rotor and the stator. + + ### 1.1 Working Principle + + An AMB system operates as a closed-loop feedback control system consisting of three main components: + + 1. **Sensors:** Measure the radial displacement of the rotor ($x$). + 2. **Controller:** Processes the displacement signal and calculates the necessary current correction ($i$) to maintain the rotor's position (setpoint). + 3. **Actuators (Electromagnets):** Receive the current and generate the magnetic force required to pull the rotor back to the center. + + ### 1.2 Mathematical Model + + In ROSS, the electromagnetic force $F$ is linearized around a nominal operating point (bias current $i_0$ and nominal gap $g_0$). The force equation is generally described by: + + $$ + F = K_i \; i + K_s \; x + $$ + + Where: + + - $K_i$ is the **current stiffness** (Force/Current factor). + - $K_s$ is the **negative electromagnetic stiffness** (Force/Displacement factor), representing the inherent instability of the magnetic attraction. + + ## Section 2: Declaring a Magnetic Bearing in ROSS + + To use an AMB in ROSS, you must instantiate the `MagneticBearingElement` class. You need to provide physical parameters of the electromagnet and the gains for the PID controller. + """) + return + + +@app.cell +def _(): + import ross as rs + import numpy as np + n_node = 4 + # Physical parameters of the actuator + g0 = 0.001 # Node where the bearing is located + i0 = 1.0 # Nominal air gap (m) + ag = 0.0001 # Bias current (A) + nw = 200 # Pole area (m²) + _alpha = np.pi / 8 # Number of windings + _Kp = 1500 # Pole angle (rad) + _Kd = 10 + # PID Controller gains + _Ki = 100 # Proportional gain + # Instantiation + amb = rs.MagneticBearingElement(n=n_node, g0=g0, i0=i0, ag=ag, nw=nw, alpha=_alpha, kp_pid=_Kp, kd_pid=_Kd, ki_pid=_Ki, tag='AMB_1') # Derivative gain # Integral gain + return np, rs + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 3: Evaluating Time Response + + Since AMBs are active control systems that calculate forces based on instantaneous states, **numerical integration must be performed using the Newmark method**. + + When `method="newmark"` is selected, ROSS calls the `magnetic_bearing_controller` loop at every time step. This calculates the error, updates the controller state, and applies the resulting magnetic force to the rotor. + + **Important:** If you use other methods (like modal integration), the AMB forces might not be updated correctly as they depend on the feedback loop. + """) + return + + +@app.cell +def _(rs): + rotor = rs.rotor_amb_example() + # 1. Create the rotor with the AMB element + rotor.plot_rotor(nodes=999).show() + return (rotor,) + + +@app.cell +def _(np, rotor, rs): + # Define the node to observe (e.g., node 0) + obs_node = 12 + force_node = 28 + speed = 500.0 + # 2. Define simulation parameters + t = np.arange(0, 3, 0.001) # rad/s + _F = np.zeros((len(t), rotor.ndof)) # Time vector + index = np.nonzero(t > 0.5)[0][0] # External force vector (e.g., 0 for free response) + _F[index, rotor.number_dof * force_node + 1] = 10 + # Impulse force in y-axis at 0.5 seconds + response = rotor.run_time_response(speed, _F, t, method='newmark') + probe_y = rs.Probe(node=obs_node, angle=np.pi / 2) + _fig = response.plot_1d(probe=[probe_y]) + # 3. Run time response explicitly using Newmark + # This ensures the AMB control loop is active at every step + # 4. Plot time response (1D) + # Define a probe at the desired node with a specific angle (π/2 rad = Y axis) + # Pass the probe as a list to the plot_1d method + _fig.show() + return (t,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 4: Collecting Forces and Control Currents + + During the `run_time_response` (with Newmark), ROSS stores the internal states of the magnetic bearing. These include the control currents calculated by the PID and the resulting magnetic forces applied to the shaft. + + These data are stored as attributes within the `MagneticBearingElement` object itself after the simulation finishes. + """) + return + + +@app.cell +def _(rotor, t): + import plotly.graph_objects as go + amb_element = rotor.bearing_elements[0] + # Access the specific bearing element from the rotor object + currents_x = amb_element.control_signal[0] + currents_y = amb_element.control_signal[1] + # Retrieve data (stored as lists for each time step) + # The structure is {amb_element}.{signal_name}[axis_index] + # Axis index: 0 for x, 1 for y. + forces_x = amb_element.magnetic_force_xy[0] + # 1. Control Signal (Currents in Amperes) + forces_y = amb_element.magnetic_force_xy[1] + _fig = go.Figure() + _fig.add_trace(go.Scatter(x=t, y=currents_x, mode='lines', name='Courent X axis')) + # 2. Magnetic Forces (Newtons) + _fig.update_layout(title='Control current - X axis', xaxis_title='Time (s)', yaxis_title='Current (A)', width=800, height=600) + # Plotting + _fig.show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 5: Sensitivity Analysis (ISO 14839) + + ### 5.1 Definition and ISO Standard + + The sensitivity function $S(j\omega)$ determines the robustness of the control system. According to **ISO 14839-3**, the sensitivity is the ratio of the sensor output to a disturbance added to the sensor signal. It represents how much a disturbance is amplified by the feedback loop. + + $$ + S(j\omega) = \frac{1}{1 + G(j\omega)H(j\omega)} + $$ + + The ISO standard classifies the stability margin based on the **peak sensitivity value ($S_{max}$)**: + + - **Zone A ($S_{max} < 3.0$):** Unrestricted operation (approx 9.5 dB). + - **Zone B ($3.0 < S_{max} < 4.0$):** Restricted operation. + - **Zone C ($S_{max} > 4.0$):** Evaluation required, potential instability. + + ### 5.2 Simulation Procedure in ROSS + + To calculate the sensitivity function, ROSS performs a specific time-domain simulation: + + 1. A **Logarithmic Chirp Signal** (sweeping sine wave) is injected as a disturbance into the control loop at the AMB sensor location. + 2. The system response (displacement) is recorded. + 3. A Frequency Response Function (FRF) is computed between the **Output (Disturbed Signal)** and the **Input (Excitation Signal)**. + """) + return + + +@app.cell +def _(rotor): + # Run sensitivity analysis + # This automatically performs the chirp injection and FFT computation + sensitivity_results = rotor.run_amb_sensitivity( + speed=0, + t_max=45, # Duration must be long enough for the chirp + dt=1e-3, # Time step + disturbance_amplitude=1e-5, + disturbance_min_frequency=0.001, # Hz + disturbance_max_frequency=150, + amb_tags=["Magnetic Bearing 0"], + verbose=0, + ); + return (sensitivity_results,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ### 5.3 The `SensitivityResults` Object + + The `run_amb_sensitivity` method returns a `SensitivityResults` object. Below is a summary of its attributes and methods. + + #### Attributes Table + + | Attribute | Type | Description | + | --- | --- | --- | + | `sensitivities` | `dict` | Dictionary containing the complex Sensitivity FRF ($S(j\omega)$) for each AMB and axis ($x$, $y$). | + | `sensitivities_abs` | `dict` | The magnitude (absolute value) of the sensitivity function. | + | `sensitivities_phase` | `dict` | The phase angle of the sensitivity function. | + | `sensitivities_frequencies` | `np.ndarray` | The frequency vector (in Hz) corresponding to the sensitivity arrays. | + | `max_abs_sensitivities` | `dict` | The peak sensitivity value ($S_{max}$) for each AMB/axis. Used for ISO classification. | + | `sensitivity_run_time_results` | `dict` | Contains raw time-domain arrays: `excitation_signal` (chirp), `disturbed_signal` (input to controller), and `sensor_signal`. | + | `sensitivity_compute_dofs` | `dict` | Mapping of AMB tags to their corresponding Degree of Freedom indices. | + + #### Accessing Results Data + + The attributes within the `SensitivityResults` object (such as `max_abs_sensitivities`, `sensitivities`, `sensitivities_abs`, and `sensitivities_phase`) are organized as **nested dictionaries**. This structure allows you to easily retrieve data for a specific magnetic bearing and a specific axis. + + **Structure Hierarchy:** + + 1. **First Level Key:** The AMB tag (string), exactly as defined when creating the `MagneticBearingElement`. + 2. **Second Level Key:** The axis (string), either `"x"` or `"y"`. + 3. **Value:** The corresponding data (scalar for max sensitivity, or array for FRFs). + + **Visual Representation:** + + ```python + sensitivity_results.max_abs_sensitivities = { + "AMB_Tag_1": { + "x": , + "y": + }, + "AMB_Tag_2": { + "x": , + "y": + } + # ... + } + ``` + + **Example: Retrieving Maximum Sensitivity** + + The following example demonstrates how to programmatically access the maximum absolute sensitivity ($S_{max}$) for a specific axis of a specific bearing to check against ISO limits. + """) + return + + +@app.cell +def _(sensitivity_results): + # Assume 'sensitivity_results' is the object returned by rotor.run_amb_sensitivity() + + # 1. Define the target Bearing Tag and Axis + target_amb_tag = ( + "Magnetic Bearing 0" # Must match the 'tag' used in MagneticBearingElement + ) + target_axis = "x" + + # 2. Access the nested dictionary + s_max = sensitivity_results.max_abs_sensitivities[target_amb_tag][target_axis] + + print(f"Peak Sensitivity for {target_amb_tag} ({target_axis}-axis): {s_max:.4f}") + + # 3. Check against ISO 14839-3 Zone A limit + if s_max < 3.0: + print("Result: Zone A (Unrestricted Operation)") + else: + print("Result: Zone B or C (Evaluation required)") + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Plotting Methods + + **`.plot()`** + + Displays the Bode plot (Magnitude and Phase) of the sensitivity function. This is the primary tool to check compliance with ISO 14839. + """) + return + + +@app.cell +def _(sensitivity_results): + # Plot Bode diagram of Sensitivity + fig_bode = sensitivity_results.plot( + frequency_units="Hz", + magnitude_scale="decibel", # Useful to check margins in dB + xaxis_type="log", + ) + fig_bode.show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **`.plot_time_results()`** + + Displays the raw time-domain signals used to calculate the sensitivity. This is useful for debugging to ensure the chirp signal was applied correctly and the system remained stable during the test. + """) + return + + +@app.cell +def _(sensitivity_results): + # Plot the Chirp injection and system response + fig_time = sensitivity_results.plot_time_results() + fig_time.show() + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 6: Implementing Complex Controllers (Cascade) + + ROSS supports arbitrary transfer functions using the `python-control` library. You can combine controllers (e.g., in series/cascade) by multiplying their transfer functions. + + **Example:** A PID controller cascaded with a **Lead Compensator** (to improve phase margin). + """) + return + + +@app.cell +def _(rs): + import control as ct + s = rs.MagneticBearingElement.s + (_Kp, _Ki, _Kd) = (1500, 100, 10) + TF_PID = _Kp + _Ki / s + _Kd * s + _alpha = 3.0 + T = 0.001 + TF_Lead = (_alpha * T * s + 1) / (T * s + 1) + TF_Combined = TF_PID * TF_Lead + amb_cascade = rs.MagneticBearingElement(n=4, g0=0.001, i0=1.0, ag=0.0001, nw=200, controller_transfer_function=TF_Combined, tag='AMB_Cascade') + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ROSS handles the discretization and state-space conversion of `TF_Combined` automatically during the simulation. + + It is worth noting that, if necessary, the `MagneticBearingElement` can still be defined by specifying only the `kp_pid`, `ki_pid`, and `kd_pid` parameters. + + + ### 6.1 Auxiliary Methods for Defining and Evaluating Transfer Functions + """) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + #### Conventions and returned types + + All “controller builder” functions return **python-control transfer functions** (`control.TransferFunction`), except where noted: + + * `pid(...)` → `TransferFunction` + * `lead_lag(...)` → `TransferFunction` + * `second_order(...)` → `TransferFunction` + * `low_pass_filter(...)` → `TransferFunction` + * `notch_filter(...)` → `TransferFunction` + * `lqg(...)` → `TransferFunction` (converted from state-space to TF internally) + * `combine(*args)` → product of transfer functions (series connection) + """) + return + + +@app.cell +def _(): + from ross.bearings.magnetic.controllers import pid, lqg, lead_lag, second_order, low_pass_filter, notch_filter, combine, plot_frequency_response + + return ( + combine, + lead_lag, + low_pass_filter, + lqg, + notch_filter, + pid, + plot_frequency_response, + second_order, + ) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + Below, each of the auxiliary methods introduced above is described in detail. + + **`pid(k_p, k_i, k_d, n_f=10000)`** + + Builds a PID controller with a **filtered derivative** term. + + * `k_p` *(float)*: proportional gain + * `k_i` *(float)*: integral gain + * `k_d` *(float)*: derivative gain + * `n_f` *(float, optional)*: derivative filter “corner” scaling (higher → closer to ideal derivative without filtering) + """) + return + + +@app.cell +def _(pid): + C = pid(k_p=3.0, k_i=5.0, k_d=0.02, n_f=300) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **`lqg(A, B, C, Q_lqr, R_lqr, Q_kalman, R_kalman)`** + + Builds an **LQG controller** (LQR state feedback + Kalman filter) and returns it as a transfer function. + + * `A`, `B`, `C`: system matrices (array-like). Converted to `float` numpy arrays. + * `Q_lqr`, `R_lqr`: LQR weighting matrices. + * `Q_kalman`, `R_kalman`: process/measurement noise covariances for the Kalman filter design. + + **Notes / tips** + + * Dimensions must match python-control expectations, where `n`, `m`, and `p` are, respectively, the number of states, controls, and outputs.: + * `A` is `(n, n)`, `B` is `(n, m)`, `C` is `(p, n)` + * `Q_lqr` is `(n, n)`, `R_lqr` is `(m, m)` + * `Q_kalman` is `(n, n)`, `R_kalman` is `(p, p)` + """) + return + + +@app.cell +def _(lqg, np): + A = [[0, 1], [-2, -0.5]] + B = [[0], [1]] + C_1 = [[1, 0]] + Q_lqr = np.diag([10, 1]) + R_lqr = np.array([[1]]) + Q_kalman = np.diag([0.1, 0.1]) + R_kalman = np.array([[0.5]]) + C_lqg = lqg(A, B, C_1, Q_lqr, R_lqr, Q_kalman, R_kalman) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **`lead_lag(tau, alpha, k=1.0)`** + + Creates a first-order lead/lag compensator: + + $$C(s)=\frac{k(\tau s + 1)}{\alpha \tau s + 1}$$ + + * `tau` *(float)*: time constant + * `alpha` *(float)*: pole/zero separation factor + * **Lead** is typically `0 < alpha < 1` (pole farther left than zero) + * **Lag** is typically `alpha > 1` (pole closer to origin than zero) + * `k` *(float, optional)*: gain multiplier + """) + return + + +@app.cell +def _(lead_lag): + C_lead = lead_lag(tau=0.02, alpha=0.1, k=2.0) + C_lag = lead_lag(tau=0.5, alpha=5.0, k=1.0) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **`second_order(b2, b1, b0, a1, a0)`** + + Creates a generic second-order transfer function: + + $$H(s)=\frac{b_2 s^2 + b_1 s + b_0}{s^2 + a_1 s + a_0}$$ + """) + return + + +@app.cell +def _(second_order): + H = second_order(b2=1, b1=0.2, b0=10, a1=0.5, a0=25) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **`low_pass_filter(w_c, k=1.0)`** + + First-order low-pass filter: + + $$F(s)=\frac{k\,\omega_c}{s + \omega_c}$$ + + * `w_c` *(float)*: cutoff frequency in rad/s + * `k` *(float, optional)*: DC gain factor + """) + return + + +@app.cell +def _(low_pass_filter): + _F = low_pass_filter(w_c=200, k=1.0) + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **`notch_filter(w_n, zeta_z, zeta_p, k=1.0)`** + + Second-order notch filter (zeros and poles around the same natural frequency): + + $$N(s)=k\frac{s^2 + 2\zeta_z\omega_n s + \omega_n^2}{s^2 + 2\zeta_p\omega_n s + \omega_n^2}$$ + + * `w_n` *(float)*: notch center frequency (rad/s) + * `zeta_z` *(float)*: zero damping (controls notch depth/shape) + * `zeta_p` *(float)*: pole damping (controls bandwidth/sharpness) + * `k` *(float, optional)*: gain multiplier + """) + return + + +@app.cell +def _(notch_filter): + N = notch_filter(w_n=120, zeta_z=0.01, zeta_p=0.2, k=1.0) + return (N,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **`combine(*args)`** + + Multiplies transfer functions in series. + + * `*args`: any number of `control.TransferFunction` objects + """) + return + + +@app.cell +def _(combine, lead_lag, low_pass_filter, notch_filter, pid): + C_2 = combine(pid(2, 1, 0.02, n_f=200), lead_lag(0.03, 0.1), notch_filter(120, 0.02, 0.2), low_pass_filter(400)) + return (C_2,) + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + **`plot_frequency_response(*systems, **kwargs)`** + + Plots magnitude (dB) and phase (degrees) for one or more systems using Plotly. + + * `*systems`: one or more LTI systems compatible with `ct.frequency_response(...)` (`control.TransferFunction` or `control.StateSpace`) + * `w_min` *(float, default=1e-2)*: minimum frequency (rad/s) + * `w_max` *(float, default=1e3)*: maximum frequency (rad/s) + * `n_points` *(int, default=1000)*: number of log-spaced frequency points + * `title` *(str, default="Frequency Response")*: plot title + * `legends` *(list[str] | None)*: legend labels; must match number of systems + """) + return + + +@app.cell +def _(C_2, N, plot_frequency_response): + plot_frequency_response(C_2, N, C_2 * N, legends=['Controller', 'Filter', 'Combined'], w_min=0.1, w_max=100000.0, n_points=1500, title='Bode (Controller / Filter / Combined)') + return + + +@app.cell(hide_code=True) +def _(mo): + mo.md(r""" + ## Section 7: Equivalent Stiffness Calculation + + For linear frequency-domain analyses (e.g., `run_modal`, `run_campbell`, `run_ucs`), ROSS cannot evaluate the discrete time-domain controller states directly. Instead, the `MagneticBearingElement` computes **frequency-dependent equivalent coefficients** that represent the closed-loop AMB behavior in the form of an equivalent stiffness and damping. + + With the updated implementation, the **displacement sensor gain** (`k_sense`, in V/m) and the **power amplifier gain** (`k_amp`, in V/A) are explicitly included in the loop gain used to build these equivalent coefficients. + + Internally, the element performs: + + 1. Build (or retrieve) the continuous-time controller transfer function $C(s)$ (PID with derivative filter or a custom transfer function). + 2. Evaluate its frequency response $C(j\omega)$ over a frequency grid $\omega$ (rad/s). + 3. Split the response into real and imaginary parts: + $ + C(j\omega)=\Re\{C(j\omega)\} + j \, \Im\{C(j\omega)\} + $ + 4. Map these parts into equivalent stiffness and damping using the electromagnetic constants: + * $K_s$: negative electromagnetic stiffness (from force linearization) + * $K_i$: current-to-force gain (from force linearization) + + ### Equivalent stiffness + + The equivalent stiffness includes the open-loop negative stiffness $K_s$ plus the real part of the controller contribution scaled by the sensor and amplifier gains: + + $$K_{eq}(\omega) = K_s + K_i , k_{amp}, k_{sense},\Re\{C(j\omega)\}$$ + + ### Equivalent damping + + The equivalent damping is formed from the imaginary part of the controller contribution, scaled by the same gains and divided by $\omega$: + + $$ + C_{eq}(\omega) = \frac{K_i , k_{amp}, k_{sense}}{\omega},\Im\{C(j\omega\} + $$ + + ### Notes on implementation details + + * In the code, $C(j\omega)$ is obtained from `control.frequency_response()`, stored as `Hjw`, and then: + + * `C_real = Hjw.real` + * `C_imag = Hjw.imag` + * The arrays are computed as: + $$ + k_{eq} = K_s + K_i,k_{amp},k_{sense},C_{real} + \qquad + c_{eq} = K_i,k_{amp},k_{sense},C_{imag},\frac{1}{\omega} + $$ + * After that, the element transforms the isotropic equivalent coefficients ${k_{eq}, c_{eq}}$ into the rotor $x\text{–}y$ coordinates using the rotation defined by `sensors_axis_rotation`, producing $k_{xx}, k_{xy}, k_{yx}, k_{yy}$ and $c_{xx}, c_{xy}, c_{yx}, c_{yy}$. + + These frequency-dependent arrays are stored in the element. During calls like `run_modal` (at a given speed), ROSS interpolates the coefficients at the required excitation frequencies to assemble the global stiffness and damping matrices for the rotor model. + """) + return + + +if __name__ == "__main__": + app.run() diff --git a/docs/user_guide/tutorial_part_5.rst b/docs/user_guide/tutorial_part_5.rst new file mode 100644 index 000000000..b5483e7c8 --- /dev/null +++ b/docs/user_guide/tutorial_part_5.rst @@ -0,0 +1,5 @@ +Tutorial Part 5 +=============== + +.. marimo:: tutorial_part_5.py + :height: 700px diff --git a/pyproject.toml b/pyproject.toml index 53cdf0ef1..6b050da28 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,7 @@ version = {attr = "ross.__version__"} "sphinxcontrib-bibtex>=2.2", "ruff", "sphinx-design", + "marimo", ] [project.scripts]