├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── build.yml │ ├── docs.yml │ ├── pr-checks.yml │ └── release.yml ├── .gitignore ├── AUTHORS.md ├── CHANGELOG.md ├── CITATION.cff ├── LICENSE ├── README.md ├── data ├── umat │ ├── umat-hooke-iso.f │ └── umat-hooke-transversaliso.f ├── units_consistent.csv ├── units_consistent_2.csv └── units_magnitude.csv ├── docs ├── _images │ ├── CollaborationWorkflow.jpg │ ├── CollaborationWorkflow_example.jpg │ ├── basic_workflow.jpg │ ├── fork.png │ ├── registration.jpg │ ├── workflow_1.png │ └── workflow_2.jpg ├── _static │ └── PLACEHOLDER ├── api │ ├── compas_fea2.job.rst │ ├── compas_fea2.model.rst │ ├── compas_fea2.problem.rst │ ├── compas_fea2.results.rst │ ├── compas_fea2.units.rst │ └── index.rst ├── backends │ └── index.rst ├── conf.py ├── development │ └── index.rst ├── index.rst └── userguide │ ├── __old │ └── gettingstarted.intro.rst │ ├── acknowledgements.rst │ ├── basics.analysis.rst │ ├── basics.data.rst │ ├── basics.model.rst │ ├── basics.overview.rst │ ├── basics.problem.rst │ ├── basics.results.rst │ ├── basics.visualisation.rst │ ├── gettingstarted.installation.rst │ ├── gettingstarted.intro.rst │ ├── gettingstarted.nextsteps.rst │ ├── gettingstarted.requirements.rst │ ├── index.rst │ ├── license.rst │ ├── units_consistent.csv │ ├── units_consistent_2.csv │ └── units_magnitude.csv ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── scripts └── PLACEHOLDER ├── src └── compas_fea2 │ ├── UI │ ├── __init__.py │ └── viewer │ │ ├── __init__.py │ │ ├── drawer.py │ │ ├── primitives.py │ │ ├── scene.py │ │ └── viewer.py │ ├── __init__.py │ ├── base.py │ ├── cli.py │ ├── job │ ├── __init__.py │ └── input_file.py │ ├── model │ ├── __init__.py │ ├── bcs.py │ ├── connectors.py │ ├── constraints.py │ ├── elements.py │ ├── groups.py │ ├── ics.py │ ├── interactions.py │ ├── interfaces.py │ ├── materials │ │ ├── __init__.py │ │ ├── concrete.py │ │ ├── material.py │ │ ├── steel.py │ │ └── timber.py │ ├── model.py │ ├── nodes.py │ ├── parts.py │ ├── releases.py │ ├── sections.py │ └── shapes.py │ ├── problem │ ├── __init__.py │ ├── combinations.py │ ├── displacements.py │ ├── fields.py │ ├── loads.py │ ├── problem.py │ ├── steps │ │ ├── __init__.py │ │ ├── dynamic.py │ │ ├── perturbations.py │ │ ├── quasistatic.py │ │ ├── static.py │ │ └── step.py │ └── steps_combinations.py │ ├── results │ ├── __init__.py │ ├── database.py │ ├── fields.py │ ├── histories.py │ ├── modal.py │ └── results.py │ ├── units │ ├── __init__.py │ ├── constants_en.txt │ └── fea2_en.txt │ └── utilities │ ├── __init__.py │ └── _utils.py ├── tasks.py ├── temp └── PLACEHOLDER └── tests ├── test_bcs.py ├── test_connectors.py ├── test_constraints.py ├── test_elements.py ├── test_groups.py ├── test_ics.py ├── test_model.py ├── test_nodes.py ├── test_parts.py ├── test_placeholder.py ├── test_releases.py ├── test_sections.py └── test_shapes.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | max_line_length = 180 12 | 13 | [*.{bat,cmd,ps1}] 14 | end_of_line = crlf 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | 19 | [*.yml] 20 | indent_size = 2 21 | 22 | [Makefile] 23 | indent_style = tab 24 | indent_size = 4 25 | 26 | [LICENSE] 27 | insert_final_newline = false 28 | 29 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at brg@arch.ethz.ch. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Context [e.g. ST3, Rhino, Blender, ...] 13 | 2. Sample script 14 | 3. Sample data 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Python version [e.g. 2.7] 26 | - Python package manager [e.g. macports, pip, conda] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | # Feature Request 7 | 8 | As a [role], I want [something] so that [benefit]. 9 | 10 | ## Details 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ### What type of change is this? 6 | 7 | - [ ] Bug fix in a **backwards-compatible** manner. 8 | - [ ] New feature in a **backwards-compatible** manner. 9 | - [ ] Breaking change: bug fix or new feature that involve incompatible API changes. 10 | - [ ] Other (e.g. doc update, configuration, etc) 11 | 12 | ### Checklist 13 | 14 | _Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._ 15 | 16 | - [ ] I added a line to the `CHANGELOG.md` file in the `Unreleased` section under the most fitting heading (e.g. `Added`, `Changed`, `Removed`). 17 | - [ ] I ran all tests on my computer and it's all green (i.e. `invoke test`). 18 | - [ ] I ran lint on my computer and there are no errors (i.e. `invoke lint`). 19 | - [ ] I added new functions/classes and made them available on a second-level import, e.g. `compas.datastructures.Mesh`. 20 | - [ ] I have added tests that prove my fix is effective or that my feature works. 21 | - [ ] I have added necessary documentation (if appropriate) 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | Build: 13 | if: "!contains(github.event.pull_request.labels.*.name, 'docs-only')" 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [macos-latest, windows-latest, ubuntu-latest] 18 | python: ["3.9", "3.10", "3.11"] 19 | 20 | steps: 21 | - uses: compas-dev/compas-actions.build@v4 22 | with: 23 | invoke_lint: true 24 | invoke_test: true 25 | python: ${{ matrix.python }} 26 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - "v*" 9 | pull_request: 10 | branches: 11 | - main 12 | 13 | jobs: 14 | docs: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: compas-dev/compas-actions.docs@v4 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | doc_url: https://compas.dev/compas_fea2/ 21 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: verify-pr-checklist 2 | on: 3 | pull_request: 4 | types: [assigned, opened, synchronize, reopened, labeled, unlabeled] 5 | branches: 6 | - main 7 | - master 8 | 9 | jobs: 10 | build: 11 | name: Check Actions 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Changelog check 16 | uses: Zomzog/changelog-checker@v1.2.0 17 | with: 18 | fileName: CHANGELOG.md 19 | checkNotification: Simple 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*" 5 | 6 | name: Create Release 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [macos-latest, windows-latest, ubuntu-latest] 14 | python: ["3.9", "3.10", "3.11"] 15 | 16 | steps: 17 | - uses: compas-dev/compas-actions.build@v4 18 | with: 19 | invoke_lint: true 20 | invoke_test: true 21 | python: ${{ matrix.python }} 22 | 23 | Publish: 24 | needs: build 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: compas-dev/compas-actions.publish@v3 28 | with: 29 | pypi_token: ${{ secrets.PYPI }} 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # ============================================================================== 104 | # compas_fea2 105 | # ============================================================================== 106 | 107 | *.3dmbak 108 | *.rhl 109 | *.rui_bak 110 | 111 | temp/** 112 | !temp/PLACEHOLDER 113 | 114 | .DS_Store 115 | 116 | .vscode 117 | 118 | docs/api/generated/ 119 | 120 | conda.recipe/ 121 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Credits 2 | 3 | ## Project Lead 4 | 5 | * Francesco Ranaudo <> [@franaudo](https://github.com/franaudo) 6 | 7 | ## Contributors 8 | 9 | None yet. Why not be the first? 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.3.0] 2025-01-09 9 | 10 | ### Added 11 | 12 | ### Changed 13 | 14 | * Changed processing of stress field results to iterate of rows grouped per part. 15 | 16 | ### Removed 17 | 18 | 19 | ## [0.2.1] 2024-05-15 20 | 21 | ### Added 22 | 23 | ### Changed 24 | 25 | ### Removed 26 | 27 | 28 | ## [0.2.0] 2024-05-13 29 | 30 | ### Added 31 | 32 | * PR checks workflow. 33 | 34 | ### Changed 35 | 36 | * Updated existing workflows to latest. 37 | * Build tests are temporarily disabled. 38 | * Updated `compas-actions.build` and `compas-actions.doc` workflows to v3. 39 | 40 | ### Removed 41 | 42 | * Support for python below 3.8 43 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.1.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: Ranaudo 5 | given-names: Francesco 6 | orcid: https://orcid.org/0000-0002-1612-5612 7 | title: compas_fea2 8 | version: v0.1 9 | date-released: 2022-08-17 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Block Research Group - ETH Zurich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # compas_fea2 2 | 3 | 2nd generation of compas_fea. Current main changes: 4 | 5 | * Plug-in architecture 6 | * Improved API for in-parallel development 7 | * Extended functionalities 8 | 9 | ## Package Objectives 10 | 11 | This package aims to create a bridge between the generation of structural geometries and their analysis using popular commercial FEA software. The geometry generation features of these software are usually limited, tedious, and time-consuming. 12 | 13 | ### Users 14 | 15 | * Simplify finite element analysis with 'pre-made' recipes to help inexperienced users get meaningful results 16 | * Better link with compas and its ecosystem 17 | * Provide a unified (as much as possible) approach across multiple backends to help researchers communicate with their industrial partners. For example, a researcher develops a structural system for a pavilion using Abaqus, but the engineer of record uses Sofistik to check the results: the analysis model for both structures can be derived from the same script with few changes 18 | * Increase the number of backend solvers supported 19 | 20 | ### Developers 21 | 22 | * Clearly separate frontend (geometry generation, problem definition, and result displaying) and backend (FEA analysis, result post-process) to enhance in-parallel development 23 | * Offer frontend and backend developers a framework to help the structuring of their modules and avoid code repetition 24 | * Provide comprehensive documentation and examples to facilitate the development and integration of new features 25 | * Ensure modularity and extensibility to allow easy addition of new functionalities and support for additional FEA software 26 | 27 | ## Installation 28 | 29 | To install compas_fea2, use the following command: 30 | 31 | ```bash 32 | pip install compas_fea2 33 | ``` 34 | 35 | ## Usage 36 | 37 | Here is a basic example of how to use compas_fea2: 38 | 39 | ```python 40 | # Import the compas_fea2 library 41 | import compas_fea2 42 | from compas_fea2.model import Model, Part, Node, Element, Material, Section 43 | from compas_fea2.problem import Problem, Step, BoundaryCondition, Pattern, Load, FieldOutput 44 | 45 | # Define a Model and its parts 46 | mdl = Model() 47 | prt_1 = Part() 48 | prt_2 = Part() 49 | # Assign the parts to the model 50 | mdl.add_parts([prt_1, prt_2]) 51 | 52 | # Define sections and materials 53 | mat = Material(E=..., v=..., density=...) 54 | sec = Section(t=..., material=mat) 55 | 56 | # Define the geometry of the structure 57 | # Specify nodes 58 | nodes_1 = [Node(xyz=...), Node(xyz=...), ...] 59 | nodes_2 = [Node(xyz=...), Node(xyz=...), ...] 60 | # Assign the nodes to a part 61 | prt_1.add_nodes(nodes_1) 62 | prt_2.add_nodes(nodes_2) 63 | 64 | # Specify elements 65 | elements_1 = [Element(nodes=[...], section=sec), Element(nodes=[...], section=sec), ...] 66 | elements_2 = [Element(nodes=[...], section=sec), Element(nodes=[...], section=sec), ...] 67 | # Assign the elements to a part 68 | prt_1.add_elements(elements_1) 69 | prt_2.add_elements(elements_2) 70 | 71 | # Define boundary conditions 72 | # Apply constraints and loads to the structure 73 | bcs = [BoundaryCondition(nodes=[...], ...), BoundaryCondition(nodes=[...], ...)] 74 | mdl.add_bcs(bcs) 75 | 76 | # Define a Problem to analyze 77 | prb = Problem() 78 | # Add the problem to the model 79 | mdl.add_problem(prb) 80 | 81 | # Define the steps of the analysis 82 | stp_1 = Step(...) 83 | stp_2 = Step(...) 84 | # Add the steps to the problem (note: this is the sequence in which they are applied) 85 | prb.add_steps([stp_1, stp_2]) 86 | 87 | # Define the load patterns 88 | pattern_1 = Pattern(nodes=[...], load=Load(...)) 89 | pattern_2 = Pattern(nodes=[...], load=Load(...)) 90 | # Add the pattern to the step 91 | stp_1.add_pattern(pattern_1) 92 | stp_2.add_pattern(pattern_2) 93 | 94 | # Define the outputs to save 95 | output = FieldOutput(...) 96 | stp_1.add_output(output) 97 | stp_2.add_output(output) 98 | 99 | # Run the analysis 100 | # Execute the finite element analysis 101 | prb.analyze_and_extract(...) 102 | 103 | # Post-process the results 104 | # Extract and visualize the results 105 | results = prb.results 106 | 107 | # View the results 108 | prb.show() 109 | ``` 110 | 111 | For more detailed examples and documentation, please refer to the official documentation. 112 | 113 | ## Contributing 114 | 115 | We welcome contributions from the community. If you would like to contribute, please follow these steps: 116 | 117 | 1. Fork the repository 118 | 2. Create a new branch (`git checkout -b feature-branch`) 119 | 3. Make your changes 120 | 4. Commit your changes (`git commit -am 'Add new feature'`) 121 | 5. Push to the branch (`git push origin feature-branch`) 122 | 6. Create a new Pull Request 123 | 124 | ## License 125 | 126 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. 127 | -------------------------------------------------------------------------------- /data/umat/umat-hooke-iso.f: -------------------------------------------------------------------------------- 1 | subroutine umat(stress,statev,ddsdde,sse,spd,scd, 2 | & rpl,ddsddt,drplde,drpldt, 3 | & stran,dstran,time,dtime,temp,dtemp,predef,dpred,cmname, 4 | & ndi,nshr,ntens,nstatv,props,nprops,coords,drot,pnewdt, 5 | & celent,dfgrd0,dfgrd1,noel,npt,layer,kspt,kstep,kinc) 6 | !---- Deklaration ABAQUS 7 | implicit none 8 | integer kstep,kspt,layer,npt,noel,nprops,nstatv,ntens, 9 | & nshr,ndi,kinc 10 | double precision sse,spd,scd,rpl,drpldt,dtime,temp,dtemp, 11 | & pnewdt,celent, dfgrd0(3,3),dfgrd1(3,3),time(2),stress(ntens), 12 | & statev(nstatv),ddsdde(ntens,ntens),ddsddt(ntens),drplde(ntens), 13 | & stran(ntens),dstran(ntens),predef(1),dpred(1),props(nprops), 14 | & coords(3),drot(3,3) 15 | character*80 cmname 16 | !---- Lokale Deklarationen 17 | integer i 18 | double precision E,G,lambda,nu,spEps,eps(6),zero,one,two 19 | !---- Nuetzliche Zahlen 20 | parameter(zero=0d0, one=1d0, two=2d0) 21 | 22 | !---- Elastische Konstanten 23 | E = PROPS(1) ! E-Modul 24 | nu = PROPS(2) ! Querkontraktionszahl 25 | G = (one/two)*E/(one+nu) ! 2. Lame Konstante (Schubmodul) 26 | lambda = two*G*nu/(one-two*nu) ! 1. Lame Konstante 27 | !---- Dehnungstensor zum aktuellen Zeitpunkt 28 | eps = stran + dstran 29 | !---- Spur des Dehnungstensors 30 | spEps = sum(eps(1:3)) 31 | !---- Spannungstensor fuer isotropes elastisches Gesetz in ABAQUS-Notation 32 | stress(1:3) = lambda*spEps + 2*G*eps(1:3) 33 | stress(4:6) = G*eps(4:6) 34 | !---- Steifigkeitsmatrix in ABAQUS-Notation 35 | ! ddsdde = ...schon definiert? => ddsdde(ntens,ntens) 36 | ! ddsdde = zero 37 | ddsdde(1:3,1:3) = lambda 38 | do i = 1,3 39 | ddsdde(i,i) = ddsdde(i,i) + 2*G 40 | end do 41 | do i = 4,6 42 | ddsdde(i,i) = G 43 | end do 44 | 45 | end 46 | -------------------------------------------------------------------------------- /data/umat/umat-hooke-transversaliso.f: -------------------------------------------------------------------------------- 1 | subroutine umat(stress,statev,ddsdde,sse,spd,scd, 2 | & rpl,ddsddt,drplde,drpldt, 3 | & stran,dstran,time,dtime,temp,dtemp,predef,dpred,cmname, 4 | & ndi,nshr,ntens,nstatv,props,nprops,coords,drot,pnewdt, 5 | & celent,dfgrd0,dfgrd1,noel,npt,layer,kspt,kstep,kinc) 6 | !---- Deklaration ABAQUS 7 | implicit none 8 | integer kstep,kspt,layer,npt,noel,nprops,nstatv,ntens, 9 | & nshr,ndi,kinc 10 | double precision sse,spd,scd,rpl,drpldt,dtime,temp,dtemp, 11 | & pnewdt,celent, dfgrd0(3,3),dfgrd1(3,3),time(2),stress(ntens), 12 | & statev(nstatv),ddsdde(ntens,ntens),ddsddt(ntens),drplde(ntens), 13 | & stran(ntens),dstran(ntens),predef(1),dpred(1),props(nprops), 14 | & coords(3),drot(3,3) 15 | character*80 cmname 16 | !---- Lokale Deklarationen 17 | integer i,j 18 | double precision c1111,c2222,c1122,c2233,c1212,c2323,eps(6), 19 | & zero,one,two 20 | !---- Nuetzliche Zahlen 21 | parameter(zero=0d0, one=1d0, two=2d0) 22 | 23 | !---- Dehnungstensor zum aktuellen Zeitpunkt 24 | eps = stran + dstran 25 | !--- Einlesen der Materialeingeschaften 26 | c1111 = props(1) 27 | c2222 = props(2) 28 | c1122 = props(3) 29 | c2233 = props(4) 30 | c1212 = props(5) 31 | !--- Berechnen der sechsten abhaengigen Konstante 32 | c2323 = (one/two)*(c2222-c2233) 33 | !--- Konstanten im Feld ddsdde speichern 34 | ddsdde(1,1:6) = [ c1111,c1122,c1122,zero ,zero ,zero ] 35 | ddsdde(2,1:6) = [ c1122,c2222,c2233,zero ,zero ,zero ] 36 | ddsdde(3,1:6) = [ c1122,c2233,c2222,zero ,zero ,zero ] 37 | ddsdde(4,1:6) = [ zero ,zero ,zero ,c1212,zero ,zero ] 38 | ddsdde(5,1:6) = [ zero, zero, zero ,zero ,c1212,zero ] 39 | ddsdde(6,1:6) = [ zero, zero, zero, zero, zero, c2323] 40 | !--- Spannung ausrechnen 41 | !stress = zero 42 | do i=1,6 43 | do j=1,6 44 | stress(i)= stress(i) + ddsdde(i,j)*eps(j) 45 | end do 46 | end do 47 | 48 | end 49 | -------------------------------------------------------------------------------- /data/units_consistent.csv: -------------------------------------------------------------------------------- 1 | Quantity, SI, SI (mm), US Unit (ft), US Unit (inch) 2 | Length, m, mm, ft, in 3 | Force, N, N, lbf, lbf 4 | Mass, kg, tonne (103 kg), slug, lbf s2/in 5 | Time, s, s, s, s 6 | Stress, Pa (N/m2), MPa (N/mm2), lbf/ft2, psi (lbf/in2) 7 | Energy, J, mJ (10−3 J), ft lbf, in lbf 8 | Density, kg/m3, tonne/mm3, slug/ft3, lbf s2/in4 9 | -------------------------------------------------------------------------------- /data/units_consistent_2.csv: -------------------------------------------------------------------------------- 1 | MASS, LENGTH, TIME, FORCE, STRESS, ENERGY 2 | kg, m, s, N, Pa, J 3 | kg, mm, ms, kN, GPa, kN-mm 4 | ton, mm, s, N, MPa, N-mm 5 | lbf-s²/in, in, s, lbf, psi, lbf-in 6 | slug, ft, s, lbf, psf, lbf-ft 7 | -------------------------------------------------------------------------------- /data/units_magnitude.csv: -------------------------------------------------------------------------------- 1 | Type, Commonly used unit, SI value, SI-mm value, Multiplication factor from commonly used to SI-mm 2 | Stiffness of Steel, 210 GPa, 210∙10^9 Pa , 210000 MPa, 1000 3 | Stiffness of Concrete, 30 GPa, 30∙10^9 Pa , 30000 MPa, 1000 4 | Density of steel, 7850 kg/m3, 7850 kg/m3,7.85∙10^-9 tonne/mm3, 10^-12 5 | Density of concrete, 2400 kg/m3, 2400 kg/m3,2.4∙10^-9 tonne/mm3, 10^-12 6 | Gravitational constant, 9.81 m/s2, 9.81 m/s2,9810 mm/s2, 1000 7 | Pressure, 1 bar, 10^5 Pa, 0.1 MPa, 10^-1 8 | Absolute zero temperature, -273.15 ̊C, 0 K, C and K both acceptable, - 9 | Stefan-Boltzmann constant, 5.67∙10-8 W∙m-2∙K-4, , 5.67∙10-11 mW∙mm-2∙K-4, 0.001 10 | Universal gas constant, 8.31 J∙K-1∙mol-1, ,8.31∙103 mJ∙K-1∙mol-1,1000 11 | -------------------------------------------------------------------------------- /docs/_images/CollaborationWorkflow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/docs/_images/CollaborationWorkflow.jpg -------------------------------------------------------------------------------- /docs/_images/CollaborationWorkflow_example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/docs/_images/CollaborationWorkflow_example.jpg -------------------------------------------------------------------------------- /docs/_images/basic_workflow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/docs/_images/basic_workflow.jpg -------------------------------------------------------------------------------- /docs/_images/fork.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/docs/_images/fork.png -------------------------------------------------------------------------------- /docs/_images/registration.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/docs/_images/registration.jpg -------------------------------------------------------------------------------- /docs/_images/workflow_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/docs/_images/workflow_1.png -------------------------------------------------------------------------------- /docs/_images/workflow_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/docs/_images/workflow_2.jpg -------------------------------------------------------------------------------- /docs/_static/PLACEHOLDER: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/docs/_static/PLACEHOLDER -------------------------------------------------------------------------------- /docs/api/compas_fea2.job.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | job 3 | ******************************************************************************** 4 | 5 | .. currentmodule:: compas_fea2.job 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | InputFile 11 | ParametersFile 12 | -------------------------------------------------------------------------------- /docs/api/compas_fea2.model.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | model 3 | ******************************************************************************** 4 | 5 | .. currentmodule:: compas_fea2.model 6 | 7 | Model 8 | ===== 9 | 10 | .. autosummary:: 11 | :toctree: generated/ 12 | 13 | Model 14 | 15 | Parts 16 | ===== 17 | 18 | .. autosummary:: 19 | :toctree: generated/ 20 | 21 | Part 22 | RigidPart 23 | 24 | Nodes 25 | ===== 26 | 27 | .. autosummary:: 28 | :toctree: generated/ 29 | 30 | Node 31 | 32 | Elements 33 | ======== 34 | 35 | .. autosummary:: 36 | :toctree: generated/ 37 | 38 | _Element 39 | MassElement 40 | BeamElement 41 | SpringElement 42 | TrussElement 43 | StrutElement 44 | TieElement 45 | ShellElement 46 | MembraneElement 47 | _Element3D 48 | TetrahedronElement 49 | HexahedronElement 50 | 51 | Releases 52 | ======== 53 | 54 | .. autosummary:: 55 | :toctree: generated/ 56 | 57 | _BeamEndRelease 58 | BeamEndPinRelease 59 | BeamEndSliderRelease 60 | 61 | Constraints 62 | =========== 63 | 64 | .. autosummary:: 65 | :toctree: generated/ 66 | 67 | _Constraint 68 | _MultiPointConstraint 69 | TieMPC 70 | BeamMPC 71 | TieConstraint 72 | 73 | Materials 74 | ========= 75 | 76 | .. autosummary:: 77 | :toctree: generated/ 78 | 79 | _Material 80 | UserMaterial 81 | Stiff 82 | ElasticIsotropic 83 | ElasticOrthotropic 84 | ElasticPlastic 85 | Concrete 86 | ConcreteSmearedCrack 87 | ConcreteDamagedPlasticity 88 | Steel 89 | Timber 90 | 91 | Sections 92 | ======== 93 | 94 | .. autosummary:: 95 | :toctree: generated/ 96 | 97 | _Section 98 | BeamSection 99 | SpringSection 100 | AngleSection 101 | BoxSection 102 | CircularSection 103 | HexSection 104 | ISection 105 | PipeSection 106 | RectangularSection 107 | ShellSection 108 | MembraneSection 109 | SolidSection 110 | TrapezoidalSection 111 | TrussSection 112 | StrutSection 113 | TieSection 114 | MassSection 115 | 116 | Boundary Conditions 117 | =================== 118 | 119 | .. autosummary:: 120 | :toctree: generated/ 121 | 122 | _BoundaryCondition 123 | GeneralBC 124 | FixedBC 125 | PinnedBC 126 | ClampBCXX 127 | ClampBCYY 128 | ClampBCZZ 129 | RollerBCX 130 | RollerBCY 131 | RollerBCZ 132 | RollerBCXY 133 | RollerBCYZ 134 | RollerBCXZ 135 | 136 | Initial Conditions 137 | ================== 138 | 139 | .. autosummary:: 140 | :toctree: generated/ 141 | 142 | _InitialCondition 143 | InitialTemperatureField 144 | InitialStressField 145 | 146 | Groups 147 | ====== 148 | 149 | .. autosummary:: 150 | :toctree: generated/ 151 | 152 | _Group 153 | NodesGroup 154 | ElementsGroup 155 | FacesGroup 156 | PartsGroup 157 | -------------------------------------------------------------------------------- /docs/api/compas_fea2.problem.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | problem 3 | ******************************************************************************** 4 | 5 | .. currentmodule:: compas_fea2.problem 6 | 7 | Problem 8 | ======= 9 | 10 | .. autosummary:: 11 | :toctree: generated/ 12 | 13 | Problem 14 | 15 | Steps 16 | ===== 17 | 18 | .. autosummary:: 19 | :toctree: generated/ 20 | 21 | Step 22 | GeneralStep 23 | _Perturbation 24 | ModalAnalysis 25 | ComplexEigenValue 26 | StaticStep 27 | LinearStaticPerturbation 28 | BucklingAnalysis 29 | DynamicStep 30 | QuasiStaticStep 31 | DirectCyclicStep 32 | 33 | Prescribed Fields 34 | ================= 35 | 36 | .. autosummary:: 37 | :toctree: generated/ 38 | 39 | _PrescribedField 40 | PrescribedTemperatureField 41 | 42 | Loads 43 | ===== 44 | 45 | .. autosummary:: 46 | :toctree: generated/ 47 | 48 | Load 49 | PrestressLoad 50 | GravityLoad 51 | TributaryLoad 52 | HarmonicPointLoad 53 | HarmonicPressureLoad 54 | ThermalLoad 55 | 56 | Combinations 57 | ============ 58 | 59 | .. autosummary:: 60 | :toctree: generated/ 61 | 62 | LoadCombination 63 | 64 | Displacements 65 | ============= 66 | 67 | .. autosummary:: 68 | :toctree: generated/ 69 | 70 | GeneralDisplacement 71 | 72 | Load Patterns 73 | ============= 74 | 75 | .. autosummary:: 76 | :toctree: generated/ 77 | 78 | Pattern 79 | NodeLoadPattern 80 | PointLoadPattern 81 | LineLoadPattern 82 | AreaLoadPattern 83 | VolumeLoadPattern 84 | 85 | Outputs 86 | ======= 87 | 88 | .. autosummary:: 89 | :toctree: generated/ 90 | 91 | FieldOutput 92 | HistoryOutput 93 | -------------------------------------------------------------------------------- /docs/api/compas_fea2.results.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | results 3 | ******************************************************************************** 4 | 5 | .. currentmodule:: compas_fea2.results 6 | 7 | .. autosummary:: 8 | :toctree: generated/ 9 | 10 | Result 11 | DisplacementResult 12 | StressResult 13 | MembraneStressResult 14 | ShellStressResult 15 | SolidStressResult 16 | DisplacementFieldResults 17 | ReactionFieldResults 18 | StressFieldResults 19 | -------------------------------------------------------------------------------- /docs/api/compas_fea2.units.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Units 3 | ******************************************************************************** 4 | 5 | compas_fea2 can use Pint for units consistency. 6 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | API Reference 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :titlesonly: 8 | 9 | compas_fea2.model 10 | compas_fea2.problem 11 | compas_fea2.job 12 | compas_fea2.results 13 | compas_fea2.units 14 | compas_fea2.utilities 15 | 16 | -------------------------------------------------------------------------------- /docs/backends/index.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Backends 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :titlesonly: 8 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | 4 | from sphinx.writers import html, html5 5 | import sphinx_compas2_theme 6 | 7 | # -- General configuration ------------------------------------------------ 8 | 9 | project = "COMPAS FEA2" 10 | copyright = "COMPAS Association" 11 | author = "Francesco Ranaudo" 12 | package = "compas_fea2" 13 | organization = "compas-dev" 14 | 15 | master_doc = "index" 16 | source_suffix = {".rst": "restructuredtext", ".md": "markdown"} 17 | templates_path = sphinx_compas2_theme.get_autosummary_templates_path() 18 | exclude_patterns = sphinx_compas2_theme.default_exclude_patterns 19 | add_module_names = True 20 | language = "en" 21 | 22 | latest_version = sphinx_compas2_theme.get_latest_version() 23 | 24 | if latest_version == "Unreleased": 25 | release = "Unreleased" 26 | version = "latest" 27 | else: 28 | release = latest_version 29 | version = ".".join(release.split(".")[0:2]) # type: ignore 30 | 31 | # -- Extension configuration ------------------------------------------------ 32 | 33 | extensions = sphinx_compas2_theme.default_extensions 34 | 35 | # numpydoc options 36 | 37 | numpydoc_show_class_members = False 38 | numpydoc_class_members_toctree = False 39 | numpydoc_attributes_as_param_list = True 40 | numpydoc_show_inherited_class_members = False 41 | 42 | # bibtex options 43 | 44 | # autodoc options 45 | 46 | autodoc_type_aliases = {} 47 | autodoc_typehints_description_target = "documented" 48 | autodoc_mock_imports = sphinx_compas2_theme.default_mock_imports 49 | autodoc_default_options = { 50 | "undoc-members": True, 51 | "show-inheritance": True, 52 | } 53 | autodoc_member_order = "groupwise" 54 | autodoc_typehints = "description" 55 | autodoc_class_signature = "separated" 56 | 57 | autoclass_content = "class" 58 | 59 | 60 | def setup(app): 61 | app.connect("autodoc-skip-member", sphinx_compas2_theme.skip) 62 | 63 | 64 | # autosummary options 65 | 66 | autosummary_generate = True 67 | autosummary_mock_imports = sphinx_compas2_theme.default_mock_imports 68 | 69 | # graph options 70 | 71 | # plot options 72 | 73 | # intersphinx options 74 | 75 | intersphinx_mapping = { 76 | "python": ("https://docs.python.org/", None), 77 | "compas": ("https://compas.dev/compas/latest/", None), 78 | } 79 | 80 | # linkcode 81 | 82 | linkcode_resolve = sphinx_compas2_theme.get_linkcode_resolve(organization, package) 83 | 84 | # extlinks 85 | 86 | extlinks = { 87 | "rhino": ("https://developer.rhino3d.com/api/RhinoCommon/html/T_%s.htm", "%s"), 88 | "blender": ("https://docs.blender.org/api/2.93/%s.html", "%s"), 89 | } 90 | 91 | # from pytorch 92 | 93 | sphinx_compas2_theme.replace(html.HTMLTranslator) 94 | sphinx_compas2_theme.replace(html5.HTML5Translator) 95 | 96 | # -- Options for HTML output ---------------------------------------------- 97 | 98 | html_theme = "multisection" 99 | html_title = project 100 | html_sidebars = {"index": []} 101 | 102 | favicons = [ 103 | { 104 | "rel": "icon", 105 | "href": "compas.ico", 106 | } 107 | ] 108 | 109 | html_theme_options = { 110 | "external_links": [ 111 | {"name": "COMPAS Framework", "url": "https://compas.dev"}, 112 | ], 113 | "icon_links": [ 114 | { 115 | "name": "GitHub", 116 | "url": f"https://github.com/{organization}/{package}", 117 | "icon": "fa-brands fa-github", 118 | "type": "fontawesome", 119 | }, 120 | { 121 | "name": "Discourse", 122 | "url": "http://forum.compas-framework.org/", 123 | "icon": "fa-brands fa-discourse", 124 | "type": "fontawesome", 125 | }, 126 | { 127 | "name": "PyPI", 128 | "url": f"https://pypi.org/project/{package}/", 129 | "icon": "fa-brands fa-python", 130 | "type": "fontawesome", 131 | }, 132 | ], 133 | "switcher": { 134 | "json_url": f"https://raw.githubusercontent.com/{organization}/{package}/gh-pages/versions.json", 135 | "version_match": version, 136 | }, 137 | "logo": { 138 | "image_light": "_static/compas_icon_white.png", 139 | "image_dark": "_static/compas_icon_white.png", 140 | "text": "COMPAS FEA2", 141 | }, 142 | "navigation_depth": 2, 143 | } 144 | 145 | html_context = { 146 | "github_url": "https://github.com", 147 | "github_user": organization, 148 | "github_repo": package, 149 | "github_version": "main", 150 | "doc_path": "docs", 151 | } 152 | 153 | html_static_path = sphinx_compas2_theme.get_html_static_path() + ["_static"] 154 | html_css_files = [] 155 | html_extra_path = [] 156 | html_last_updated_fmt = "" 157 | html_copy_source = False 158 | html_show_sourcelink = True 159 | html_permalinks = False 160 | html_permalinks_icon = "" 161 | html_compact_lists = True 162 | -------------------------------------------------------------------------------- /docs/development/index.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Development 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :titlesonly: 8 | 9 | 10 | Getting started with this project 11 | ================================= 12 | 13 | Setup code editor 14 | ----------------- 15 | 16 | 1. Open project folder in VS Code 17 | 2. Select python environment for the project 18 | 19 | All terminal commands in the following sections can be run from the VS Code integrated terminal. 20 | 21 | First steps with git 22 | -------------------- 23 | 24 | 1. Go to the ``Source control`` tab 25 | 2. Make an initial commit with all newly created files 26 | 27 | First steps with code 28 | --------------------- 29 | 30 | 1. Install the newly created project 31 | 32 | .. code-block:: bash 33 | 34 | pip install -e . 35 | 36 | 2. Install it on Rhino 37 | 38 | .. code-block:: bash 39 | 40 | python -m compas_rhino.install 41 | 42 | Code conventions 43 | ---------------- 44 | 45 | Code convention follows `PEP8 `_ style guidelines and line length of 120 characters. 46 | 47 | 1. Check adherence to style guidelines 48 | 49 | .. code-block:: bash 50 | 51 | invoke lint 52 | 53 | 2. Format code automatically 54 | 55 | .. code-block:: bash 56 | 57 | invoke format 58 | 59 | Documentation 60 | ------------- 61 | 62 | Documentation is generated automatically out of docstrings and `RST `_ files in this repository 63 | 64 | 1. Generate the docs 65 | 66 | .. code-block:: bash 67 | 68 | invoke docs 69 | 70 | 2. Check links in docs are valid 71 | 72 | .. code-block:: bash 73 | 74 | invoke linkcheck 75 | 76 | 3. Open docs in your browser (file explorer -> ``dist/docs/index.html``) 77 | 78 | Testing 79 | ------- 80 | 81 | Tests are written using the `pytest `_ framework 82 | 83 | 1. Run all tests from terminal 84 | 85 | .. code-block:: bash 86 | 87 | invoke test 88 | 89 | 2. Or run them from VS Code from the ``Testing`` tab 90 | 91 | Developing Grasshopper components 92 | --------------------------------- 93 | 94 | We use `Grasshopper Componentizer `_ to develop Python components that can be stored and edited on git. 95 | 96 | 1. Build components 97 | 98 | .. code-block:: bash 99 | 100 | invoke build-ghuser-components 101 | 102 | 2. Install components on Rhino 103 | 104 | .. code-block:: bash 105 | 106 | python -m compas_rhino.install 107 | 108 | Publish release 109 | --------------- 110 | 111 | Releases follow the `semver `_ versioning convention. 112 | 113 | 1. Create a new release 114 | 115 | .. code-block:: bash 116 | 117 | invoke release major 118 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | :html_theme.sidebar_secondary.remove: 2 | 3 | ******************************************************************************** 4 | COMPAS FEA2 Documentation 5 | ******************************************************************************** 6 | 7 | .. rst-class:: lead 8 | 9 | COMPAS FEA2 is a framework for Finite Element Analysis (FEA) written in Python. 10 | It provides a high-level interface to various open-source and commercial FEA software, 11 | with a unified API that is easy to use and extend. 12 | 13 | 14 | User Guide 15 | ========== 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | :titlesonly: 20 | 21 | userguide/index 22 | 23 | 24 | API Reference 25 | ============= 26 | 27 | .. toctree:: 28 | :maxdepth: 2 29 | :titlesonly: 30 | 31 | api/index 32 | 33 | 34 | Backends 35 | ======== 36 | 37 | .. toctree:: 38 | :maxdepth: 2 39 | :titlesonly: 40 | 41 | backends/index 42 | 43 | 44 | Development 45 | =========== 46 | 47 | .. toctree:: 48 | :maxdepth: 2 49 | :titlesonly: 50 | 51 | development/index 52 | 53 | 54 | Indices and tables 55 | ================== 56 | 57 | * :ref:`genindex` 58 | * :ref:`modindex` 59 | * :ref:`search` 60 | -------------------------------------------------------------------------------- /docs/userguide/__old/gettingstarted.intro.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Introduction 3 | ******************************************************************************** 4 | 5 | Plug-in architecture 6 | ==================== 7 | 8 | ``compas_fea2`` implements a plug-in architecture. The ``compas_fea2`` main 9 | package only defines the general API for a Finite Element Analysis, while the 10 | actual implementation in the a specific backend is done in the corresponding 11 | plug-in, whcih is registered and the beginning of the analysis. Once the analysis 12 | is complente, the results are recorded in a SQL database and can be accessed by 13 | the user through the SQL wrapper provided by ``compas_fea2``, by his/her own 14 | SQL statements or through an external interface. 15 | 16 | .. .. figure:: /_images/registration.jpg 17 | .. :figclass: figure 18 | .. :class: figure-img img-fluid 19 | 20 | 21 | Workflow 22 | ======== 23 | 24 | The image below describes a general FEA workflow: 25 | 26 | .. .. figure:: /_images/basic_workflow.png 27 | .. :figclass: figure 28 | .. :class: figure-img img-fluid 29 | 30 | 31 | Collaboration Workflow 32 | ====================== 33 | 34 | The aim of ``compas_fea2`` is to create a common platform for FEA that can be shared 35 | across disciplines and software. This is achieved by standardizing the API for 36 | the creation and analysis of an FE model, and by serializing `models`, `problems` 37 | and `results` in a common database that can be easly shared. 38 | 39 | The two images below show the general collaboration workflow and a specific example 40 | of a structural engineer using rhino and abaqus collaborating with an acoustic 41 | engineer using blender and ansys: 42 | 43 | 44 | .. .. figure:: /_images/CollaborationWorkflow.jpg 45 | .. :figclass: figure 46 | .. :class: figure-img img-fluid 47 | 48 | 49 | .. .. figure:: /_images/CollaborationWorkflow_example.jpg 50 | .. :figclass: figure 51 | .. :class: figure-img img-fluid 52 | 53 | 54 | Units 55 | ===== 56 | 57 | Before starting any model, you need to decide which system of 58 | units you will use. ``compas_fea2`` has no built-in system of units. 59 | 60 | .. warning:: Units consistency 61 | 62 | All input data must be specified in consistent units. 63 | Do not include unit names or labels when entering data in ``compas_fea2``. 64 | 65 | 66 | Some common systems of consistent units are shown in the table below: 67 | 68 | .. csv-table:: Consistent Units 69 | :file: units_consistent.csv 70 | :header-rows: 1 71 | 72 | In case you do not want to follow a predefined system, you need to be consistent with 73 | your units assignemnts. Below there are some exmple of correct choices of units: 74 | 75 | .. csv-table:: Consistent Units 76 | :file: units_consistent_2.csv 77 | :header-rows: 1 78 | 79 | The order of magnitude expected for different properties is shown below: 80 | 81 | .. csv-table:: Magnitude 82 | :file: units_magnitude.csv 83 | :header-rows: 1 84 | -------------------------------------------------------------------------------- /docs/userguide/acknowledgements.rst: -------------------------------------------------------------------------------- 1 | ****************************************************************************** 2 | Acknowledgements 3 | ****************************************************************************** 4 | -------------------------------------------------------------------------------- /docs/userguide/basics.analysis.rst: -------------------------------------------------------------------------------- 1 | ****************************************************************************** 2 | Analysis 3 | ****************************************************************************** 4 | -------------------------------------------------------------------------------- /docs/userguide/basics.data.rst: -------------------------------------------------------------------------------- 1 | ****************************************************************************** 2 | Data 3 | ****************************************************************************** 4 | 5 | An analysis with COMPAS FEA2 is defined by a "model" (:class:`compas_fea2.model.Model`) 6 | and a "problem" (:class:`compas_fea2.problem.Problem`), with each many different sub-components. 7 | 8 | All these components, and the model and problem themselves, are COMPAS data objects, 9 | and derive from a base FEA2 data class (:class:`compas_fea2.base.FEAData`). 10 | 11 | .. code-block:: None 12 | 13 | compas.data.Data 14 | |_ compas_fea2.base.FEAData 15 | |_ compas_fea2.model.Model 16 | |_ compas_fea2.model.Node 17 | |_ compas_fea2.model.Element 18 | |_ ... 19 | |_ compas_fea2.model.Part 20 | |_ ... 21 | |_ compas_fea2.model.Material 22 | |_ ... 23 | |_ compas_fea2.model.Section 24 | |_ ... 25 | |_ compas_fea2.model.Constraint 26 | |_ ... 27 | |_ compas_fea2.model.Group 28 | |_ ... 29 | |_ compas_fea2.model.BoundaryCondition 30 | |_ ... 31 | |_ compas_fea2.model.InitialCondition 32 | |_ ... 33 | 34 | 35 | .. code-block:: None 36 | 37 | compas.data.Data 38 | |_ compas_fea2.base.FEAData 39 | |_ compas_fea2.problem.Problem 40 | |_ compas_fea2.problem.Step 41 | |_ ... 42 | |_ compas_fea2.problem.Load 43 | |_ ... 44 | |_ compas_fea2.problem.Displacement 45 | |_ ... 46 | 47 | 48 | This means that all these components have the same base data infrastructure as all other COMPAS objects. 49 | They have a guid, a name, and general attributes. 50 | 51 | >>> from compas_fea2.model import Node 52 | >>> node = Node(xyz=(0., 0., 0.), name='node') 53 | >>> node.name 54 | 'node' 55 | 56 | -------------------------------------------------------------------------------- /docs/userguide/basics.model.rst: -------------------------------------------------------------------------------- 1 | ****************************************************************************** 2 | Model 3 | ****************************************************************************** 4 | 5 | At the heart of every COMPAS FEA2 analysis or simulation is a model. 6 | A model consists of nodes, elements and parts, 7 | and defines connections, constraints and boundary conditions. 8 | 9 | >>> from compas_fea2.model import Model 10 | >>> model = Model() 11 | >>> 12 | 13 | Nodes 14 | ===== 15 | 16 | Nodes are the basic building blocks of a model. 17 | They define the locations in space that define all other entities. 18 | 19 | >>> from compas_fea2.model import Node 20 | >>> node = Node(xyz=(0.,0.,0.)) 21 | >>> 22 | Node(...) 23 | >>> node.x 24 | 0.0 25 | >>> node.y 26 | 0.0 27 | >>> node.z 28 | 0.0 29 | >>> node.xyz 30 | [0.0, 0.0, 0.0] 31 | >>> node.point 32 | Point(x=0.0, y=0.0, z=0.0) 33 | 34 | Besides coordinates, nodes have many other (optional) attributes. 35 | 36 | >>> node.mass 37 | [None, None, None, None, None, None] 38 | >>> node.temperature 39 | >>> 40 | >>> node.dof 41 | {'x': True, 'y': True, 'z': True, 'xx': True, 'yy': True, 'zz': True} 42 | 43 | 44 | Elements 45 | ======== 46 | 47 | Elements are defined by the nodes they connect to and a section. 48 | 49 | >>> 50 | 51 | Parts 52 | ===== 53 | -------------------------------------------------------------------------------- /docs/userguide/basics.overview.rst: -------------------------------------------------------------------------------- 1 | ****************************************************************************** 2 | Overview 3 | ****************************************************************************** 4 | 5 | compas_fea2 is a flexible and extensible framework for finite element analysis (FEA) in Python. It is part of the COMPAS framework, which is an open-source, Python-based framework for computational research and collaboration in architecture, engineering, and digital fabrication. 6 | 7 | Key features of compas_fea2 include: 8 | - **Modularity**: Easily extend and customize the framework to suit specific needs. 9 | - **Interoperability**: Seamlessly integrate with other COMPAS packages and external software. 10 | - **User-friendly**: Simplifies the setup, execution, and post-processing of FEA simulations. 11 | - **Extensive Documentation**: Comprehensive guides and examples to help users get started quickly. 12 | 13 | compas_fea2 supports various types of analyses, including: 14 | - Static analysis 15 | - Modal analysis 16 | - Buckling analysis 17 | - Dynamic analysis 18 | 19 | The package is designed to work with different FEA solvers, providing a unified interface for setting up and running simulations. This allows users to switch between solvers without changing their workflow. 20 | 21 | For more information and detailed usage instructions, please refer to the official documentation and user guides. 22 | -------------------------------------------------------------------------------- /docs/userguide/basics.problem.rst: -------------------------------------------------------------------------------- 1 | ****************************************************************************** 2 | Problem 3 | ****************************************************************************** 4 | 5 | The `Problem` class in compas_fea2 is a central component for defining and managing finite element analysis problems. It encapsulates all the necessary information required to set up and solve an FEA problem, including the model, analysis type, boundary conditions, loads, and solver settings. 6 | 7 | Key attributes and methods of the `Problem` class include: 8 | - **Attributes**: 9 | - `model`: The finite element model to be analyzed. 10 | - `analysis_type`: The type of analysis to be performed (e.g., static, modal, dynamic). 11 | - `boundary_conditions`: A list of boundary conditions applied to the model. 12 | - `loads`: A list of loads applied to the model. 13 | - `solver_settings`: Configuration settings for the FEA solver. 14 | 15 | - **Methods**: 16 | - `add_boundary_condition(bc)`: Adds a boundary condition to the problem. 17 | - `add_load(load)`: Adds a load to the problem. 18 | - `set_solver(solver)`: Sets the solver to be used for the analysis. 19 | - `solve()`: Executes the analysis using the specified solver and settings. 20 | 21 | The `Problem` class provides a high-level interface for defining and managing FEA problems, making it easier for users to set up and run simulations without dealing with low-level details. 22 | 23 | Example usage: 24 | ```python 25 | from compas_fea2 import Problem 26 | 27 | # Create a new Problem instance 28 | problem = Problem(model=my_model, analysis_type='static') 29 | 30 | # Add boundary conditions and loads 31 | problem.add_boundary_condition(my_boundary_condition) 32 | problem.add_load(my_load) 33 | 34 | # Set the solver and solve the problem 35 | problem.set_solver(my_solver) 36 | problem.solve() 37 | ``` 38 | 39 | For more detailed information and examples, please refer to the official documentation and user guides. 40 | -------------------------------------------------------------------------------- /docs/userguide/basics.results.rst: -------------------------------------------------------------------------------- 1 | ****************************************************************************** 2 | Results 3 | ****************************************************************************** 4 | 5 | In compas_fea2, the results of a finite element analysis are organized in a structured manner to facilitate easy access and post-processing. The results are typically stored in a `Results` object, which contains various types of data depending on the analysis performed. 6 | 7 | Key components of the `Results` object include: 8 | - **Nodal Results**: Displacements, velocities, accelerations, and reaction forces at the nodes. 9 | - **Element Results**: Stresses, strains, and internal forces within the elements. 10 | - **Global Results**: Overall quantities such as total reaction forces and energy values. 11 | 12 | To access the results, you can use the following methods provided by the `Results` object: 13 | - `get_nodal_displacements()`: Returns the displacements at the nodes. 14 | - `get_nodal_reactions()`: Returns the reaction forces at the nodes. 15 | - `get_element_stresses()`: Returns the stresses within the elements. 16 | - `get_element_strains()`: Returns the strains within the elements. 17 | 18 | Example usage: 19 | ```python 20 | # Assuming 'results' is an instance of the Results class 21 | nodal_displacements = results.get_nodal_displacements() 22 | nodal_reactions = results.get_nodal_reactions() 23 | element_stresses = results.get_element_stresses() 24 | element_strains = results.get_element_strains() 25 | 26 | # Process or visualize the results as needed 27 | ``` 28 | 29 | The `Results` object provides a convenient interface for accessing and manipulating the analysis results, enabling users to perform further analysis, visualization, or reporting. 30 | 31 | For more detailed information and examples, please refer to the official documentation and user guides. 32 | -------------------------------------------------------------------------------- /docs/userguide/basics.visualisation.rst: -------------------------------------------------------------------------------- 1 | ****************************************************************************** 2 | Visualisation 3 | ****************************************************************************** 4 | -------------------------------------------------------------------------------- /docs/userguide/gettingstarted.installation.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Installation 3 | ******************************************************************************** 4 | 5 | The recommended way to install ``compas_fea2`` 6 | is in a dedicated ``conda`` environment. 7 | 8 | .. code-block:: bash 9 | 10 | conda create -n fea2 compas 11 | conda activate fea2 12 | pip install compas_fea2 13 | 14 | After the installation is complete, run the built-in tests 15 | to verify that you have a functional setup with at least one working backend. 16 | 17 | .. code-block:: bash 18 | 19 | python -m compas_fea2.test 20 | -------------------------------------------------------------------------------- /docs/userguide/gettingstarted.intro.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Introduction 3 | ******************************************************************************** 4 | 5 | Welcome to compas_fea2, a powerful and flexible framework for finite element analysis (FEA) in Python. This package is part of the COMPAS framework, an open-source initiative aimed at providing computational tools for research and collaboration in architecture, engineering, and digital fabrication. 6 | 7 | ### What is Finite Element Analysis (FEA)? 8 | 9 | Finite Element Analysis (FEA) is a numerical method used to solve complex engineering problems. It involves breaking down a large system into smaller, simpler parts called finite elements. These elements are then analyzed individually, and their behavior is combined to understand the overall system's response to various conditions such as loads, constraints, and environmental factors. 10 | 11 | ### How compas_fea2 Works 12 | 13 | compas_fea2 simplifies the process of setting up, running, and post-processing FEA simulations. The package provides a high-level interface for defining models, applying loads and boundary conditions, selecting solvers, and retrieving results. It is designed to be modular and extensible, allowing users to customize and extend its functionality to meet specific needs. 14 | 15 | Key components of compas_fea2 include: 16 | - **Model**: Represents the finite element model, including nodes, elements, materials, and sections. 17 | - **Problem**: Defines the analysis problem, including the model, analysis type, boundary conditions, loads, and solver settings. 18 | - **Solver**: Executes the analysis and computes the results. 19 | - **Results**: Stores and provides access to the analysis results. 20 | 21 | ### Possible Applications 22 | 23 | compas_fea2 can be used in a wide range of applications, including but not limited to: 24 | - **Structural Analysis**: Evaluate the strength, stability, and deformation of structures under various loads. 25 | - **Modal Analysis**: Determine the natural frequencies and mode shapes of structures. 26 | - **Buckling Analysis**: Assess the buckling behavior of structures under compressive loads. 27 | - **Dynamic Analysis**: Analyze the response of structures to dynamic loads such as earthquakes and wind. 28 | - **Thermal Analysis**: Study the thermal behavior of structures and materials. 29 | 30 | Whether you are an engineer, researcher, or student, compas_fea2 provides the tools you need to perform advanced FEA simulations efficiently and effectively. 31 | 32 | For more detailed information and examples, please refer to the official documentation and user guides. 33 | 34 | -------------------------------------------------------------------------------- /docs/userguide/gettingstarted.nextsteps.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Next Steps 3 | ******************************************************************************** 4 | 5 | -------------------------------------------------------------------------------- /docs/userguide/gettingstarted.requirements.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | Requirements 3 | ******************************************************************************** 4 | 5 | COMPAS FEA2 is a high-level modelling language for finite element analysis. 6 | It uses COMPAS data structures and geometry to define analysis models and related analysis problem definitions. 7 | The actual analysis is handed off to open source solvers, such as OpenSEES or commercial analysis software, such as Abaqus. 8 | 9 | Currently the following solvers or "backends" are supported: 10 | 11 | * Abaqus 12 | * ANSYS 13 | * SOFiSTiK 14 | * OpenSEES 15 | 16 | In order to run an analysis, you need to have one of these solvers installed on your system. 17 | See :doc:`backends/index` for more information. 18 | -------------------------------------------------------------------------------- /docs/userguide/index.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | User Guide 3 | ******************************************************************************** 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | :titlesonly: 8 | :caption: Getting Started 9 | 10 | gettingstarted.intro 11 | gettingstarted.requirements 12 | gettingstarted.installation 13 | gettingstarted.nextsteps 14 | 15 | .. toctree:: 16 | :maxdepth: 2 17 | :titlesonly: 18 | :caption: Tutorial 19 | 20 | basics.overview 21 | basics.data 22 | basics.model 23 | basics.problem 24 | basics.analysis 25 | basics.results 26 | basics.visualisation 27 | 28 | .. toctree:: 29 | :maxdepth: 2 30 | :titlesonly: 31 | :caption: Miscellaneous 32 | 33 | license 34 | acknowledgements 35 | -------------------------------------------------------------------------------- /docs/userguide/license.rst: -------------------------------------------------------------------------------- 1 | ******************************************************************************** 2 | License 3 | ******************************************************************************** 4 | 5 | .. literalinclude:: ../../LICENSE 6 | -------------------------------------------------------------------------------- /docs/userguide/units_consistent.csv: -------------------------------------------------------------------------------- 1 | Quantity, SI, SI (mm), US Unit (ft), US Unit (inch) 2 | Length, m, mm, ft, in 3 | Force, N, N, lbf, lbf 4 | Mass, kg, tonne (103 kg), slug, lbf s2/in 5 | Time, s, s, s, s 6 | Stress, Pa (N/m2), MPa (N/mm2), lbf/ft2, psi (lbf/in2) 7 | Energy, J, mJ (10−3 J), ft lbf, in lbf 8 | Density, kg/m3, tonne/mm3, slug/ft3, lbf s2/in4 9 | -------------------------------------------------------------------------------- /docs/userguide/units_consistent_2.csv: -------------------------------------------------------------------------------- 1 | MASS, LENGTH, TIME, FORCE, STRESS, ENERGY 2 | kg, m, s, N, Pa, J 3 | kg, mm, ms, kN, GPa, kN-mm 4 | ton, mm, s, N, MPa, N-mm 5 | lbf-s²/in, in, s, lbf, psi, lbf-in 6 | slug, ft, s, lbf, psf, lbf-ft 7 | -------------------------------------------------------------------------------- /docs/userguide/units_magnitude.csv: -------------------------------------------------------------------------------- 1 | Type, Commonly used unit, SI value, SI-mm value, Multiplication factor from commonly used to SI-mm 2 | Stiffness of Steel, 210 GPa, 210∙10^9 Pa , 210000 MPa, 1000 3 | Stiffness of Concrete, 30 GPa, 30∙10^9 Pa , 30000 MPa, 1000 4 | Density of steel, 7850 kg/m3, 7850 kg/m3,7.85∙10^-9 tonne/mm3, 10^-12 5 | Density of concrete, 2400 kg/m3, 2400 kg/m3,2.4∙10^-9 tonne/mm3, 10^-12 6 | Gravitational constant, 9.81 m/s2, 9.81 m/s2,9810 mm/s2, 1000 7 | Pressure, 1 bar, 10^5 Pa, 0.1 MPa, 10^-1 8 | Absolute zero temperature, -273.15 ̊C, 0 K, C and K both acceptable, - 9 | Stefan-Boltzmann constant, 5.67∙10-8 W∙m-2∙K-4, , 5.67∙10-11 mW∙mm-2∙K-4, 0.001 10 | Universal gas constant, 8.31 J∙K-1∙mol-1, ,8.31∙103 mJ∙K-1∙mol-1,1000 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=66.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | # ============================================================================ 6 | # project info 7 | # ============================================================================ 8 | 9 | [project] 10 | name = "compas_fea2" 11 | description = "This package is the 2nd generation of Finite element Analysis tools for COMPAS." 12 | keywords = [] 13 | authors = [{ name = "Francesco Ranaudo", email = "francesco.ranaudo@gmail.com" }] 14 | license = { file = "LICENSE" } 15 | readme = "README.md" 16 | requires-python = ">=3.9" 17 | dynamic = ['dependencies', 'optional-dependencies', 'version'] 18 | classifiers = [ 19 | "Development Status :: 4 - Beta", 20 | "Topic :: Scientific/Engineering", 21 | "Programming Language :: Python", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | ] 27 | 28 | [project.urls] 29 | Homepage = "https://compas-dev.github.io/compas_fea2" 30 | Documentation = "https://compas-dev.github.io/compas_fea2" 31 | Repository = "https://github.com/compas-dev/compas_fea2.git" 32 | Changelog = "https://github.com/compas-dev/compas_fea2/blob/main/CHANGELOG.md" 33 | 34 | [project.scripts] 35 | fea2 = "compas_fea2.cli:main" 36 | 37 | # ============================================================================ 38 | # setuptools config 39 | # ============================================================================ 40 | 41 | [tool.setuptools] 42 | package-dir = { "" = "src" } 43 | include-package-data = true 44 | zip-safe = false 45 | 46 | [tool.setuptools.dynamic] 47 | version = { attr = "compas_fea2.__version__" } 48 | dependencies = { file = "requirements.txt" } 49 | optional-dependencies = { dev = { file = "requirements-dev.txt" } } 50 | 51 | [tool.setuptools.packages.find] 52 | where = ["src"] 53 | 54 | # ============================================================================ 55 | # replace pytest.ini 56 | # ============================================================================ 57 | 58 | [tool.pytest.ini_options] 59 | minversion = "6.0" 60 | testpaths = ["tests", "src/compas_fea2"] 61 | python_files = ["test_*.py", "*_test.py", "test.py"] 62 | addopts = ["-ra", "--strict-markers", "--doctest-glob=*.rst", "--tb=short"] 63 | doctest_optionflags = [ 64 | "NORMALIZE_WHITESPACE", 65 | "IGNORE_EXCEPTION_DETAIL", 66 | "ALLOW_UNICODE", 67 | "ALLOW_BYTES", 68 | "NUMBER", 69 | ] 70 | 71 | # ============================================================================ 72 | # replace bumpversion.cfg 73 | # ============================================================================ 74 | 75 | [tool.bumpversion] 76 | current_version = "0.3.1" 77 | message = "Bump version to {new_version}" 78 | commit = true 79 | tag = true 80 | 81 | [[tool.bumpversion.files]] 82 | filename = "src/compas_fea2/__init__.py" 83 | search = "{current_version}" 84 | replace = "{new_version}" 85 | 86 | [[tool.bumpversion.files]] 87 | filename = "CHANGELOG.md" 88 | search = "Unreleased" 89 | replace = "[{new_version}] {now:%Y-%m-%d}" 90 | 91 | # ============================================================================ 92 | # replace setup.cfg 93 | # ============================================================================ 94 | 95 | [tool.black] 96 | line-length = 179 97 | 98 | [tool.ruff] 99 | line-length = 179 100 | indent-width = 4 101 | target-version = "py39" 102 | 103 | [tool.ruff.lint] 104 | select = ["E", "F", "I"] 105 | 106 | [tool.ruff.lint.per-file-ignores] 107 | "__init__.py" = ["I001"] 108 | "tests/*" = ["I001"] 109 | "tasks.py" = ["I001"] 110 | 111 | [tool.ruff.lint.isort] 112 | force-single-line = true 113 | 114 | [tool.ruff.lint.pydocstyle] 115 | convention = "numpy" 116 | 117 | [tool.ruff.lint.pycodestyle] 118 | max-doc-length = 179 119 | 120 | [tool.ruff.format] 121 | docstring-code-format = true 122 | docstring-code-line-length = "dynamic" 123 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | attrs >=17.4 2 | black >=22.12.0 3 | bump-my-version 4 | compas_invocations2 5 | compas_notebook >=0.5.0 6 | invoke >=0.14 7 | ruff 8 | sphinx_compas2_theme 9 | twine 10 | wheel 11 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | compas>=2.0 2 | compas_gmsh 3 | compas_viewer 4 | Click 5 | matplotlib 6 | pint 7 | python-dotenv 8 | h5py 9 | -------------------------------------------------------------------------------- /scripts/PLACEHOLDER: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/scripts/PLACEHOLDER -------------------------------------------------------------------------------- /src/compas_fea2/UI/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | from .viewer import FEA2Viewer 6 | 7 | __all__ = [ 8 | "FEA2Viewer", 9 | ] 10 | -------------------------------------------------------------------------------- /src/compas_fea2/UI/viewer/__init__.py: -------------------------------------------------------------------------------- 1 | from .viewer import FEA2Viewer 2 | from .scene import FEA2ModelObject 3 | from .scene import FEA2StepObject 4 | from .scene import FEA2Stress2DFieldResultsObject 5 | from .scene import FEA2NodeFieldResultsObject 6 | 7 | from .primitives import ( 8 | _BCShape, 9 | FixBCShape, 10 | PinBCShape, 11 | RollerBCShape, 12 | ArrowShape, 13 | ) 14 | 15 | __all__ = [ 16 | "FEA2Viewer", 17 | "_BCShape", 18 | "FixBCShape", 19 | "PinBCShape", 20 | "RollerBCShape", 21 | "ArrowShape", 22 | "FEA2ModelObject", 23 | "FEA2StepObject", 24 | "FEA2Stress2DFieldResultsObject", 25 | "FEA2NodeFieldResultsObject", 26 | ] 27 | -------------------------------------------------------------------------------- /src/compas_fea2/UI/viewer/drawer.py: -------------------------------------------------------------------------------- 1 | from compas.colors import Color 2 | from compas.colors import ColorMap 3 | from compas.geometry import Line 4 | 5 | 6 | def draw_field_vectors(locations, vectors, scale_results, translate=0, high=None, low=None, cmap=None, **kwargs): 7 | """Display a given vector field. 8 | 9 | Parameters 10 | ---------- 11 | field_locations : list 12 | The locations of the field. 13 | field_results : list 14 | The results of the field. 15 | scale_results : float 16 | The scale factor for the results. 17 | translate : float 18 | The translation factor for the results. 19 | """ 20 | colors = [] 21 | lines = [] 22 | if cmap: 23 | lengths = [v.length for v in vectors] 24 | min_value = high or min(lengths) 25 | max_value = low or max(lengths) 26 | else: 27 | colors = [Color.red()] * len(vectors) 28 | 29 | for pt, vector in zip(list(locations), list(vectors)): 30 | if vector.length == 0: 31 | continue 32 | else: 33 | v = vector.scaled(scale_results) 34 | lines.append(Line.from_point_and_vector(pt, v).translated(v * translate)) 35 | if cmap: 36 | colors.append(cmap(vector.length, minval=min_value, maxval=max_value)) 37 | return lines, colors 38 | 39 | 40 | def draw_field_contour(model, field_locations, field_results, high=None, low=None, cmap=None, **kwargs): 41 | """Display a given scalar field. 42 | 43 | Parameters 44 | ---------- 45 | field_locations : list 46 | The locations of the field. 47 | field_results : list 48 | The results of the field. 49 | high : float 50 | The maximum value of the field. 51 | low : float 52 | The minimum value of the field. 53 | cmap : :class:`compas.colors.ColorMap` 54 | The color map for the field. 55 | """ 56 | # # Get values 57 | min_value = high or min(field_results) 58 | max_value = low or max(field_results) 59 | cmap = cmap or ColorMap.from_palette("hawaii") 60 | 61 | # Get mesh 62 | part_vertexcolor = {} 63 | for part in model.parts: 64 | if not part.discretized_boundary_mesh: 65 | continue 66 | # Color the mesh 67 | vertexcolor = {} 68 | gkey_vertex = part.discretized_boundary_mesh.gkey_vertex(3) 69 | for n, v in zip(field_locations, field_results): 70 | if not n.part == part: 71 | continue 72 | if kwargs.get("bound", None): 73 | if v >= kwargs["bound"][1] or v <= kwargs["bound"][0]: 74 | color = Color.red() 75 | else: 76 | color = cmap(v, minval=min_value, maxval=max_value) 77 | else: 78 | color = cmap(v, minval=min_value, maxval=max_value) 79 | vertex = gkey_vertex.get(n.gkey, None) 80 | vertexcolor[vertex] = color 81 | part_vertexcolor[part] = vertexcolor 82 | 83 | return part_vertexcolor 84 | -------------------------------------------------------------------------------- /src/compas_fea2/UI/viewer/primitives.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Box 2 | from compas.geometry import Circle 3 | from compas.geometry import Cone 4 | from compas.geometry import Cylinder 5 | from compas.geometry import Frame 6 | from compas.geometry import Plane 7 | 8 | 9 | class _BCShape: 10 | """Basic shape for reppresenting the boundary conditions. 11 | 12 | Parameters 13 | ---------- 14 | xyz : list of float 15 | The coordinates of the restrained node. 16 | direction : list of float, optional 17 | The direction of the normal. Default is [0, 0, 1]. 18 | scale : float, optional 19 | The scale factor to apply when drawing. Default is 1. 20 | """ 21 | 22 | def __init__(self, xyz, direction, scale): 23 | self.x, self.y, self.z = xyz 24 | self.direction = direction 25 | self.scale = scale 26 | 27 | 28 | class _LoadShape: 29 | """Basic shape for reppresenting the boundary conditions. 30 | 31 | Parameters 32 | ---------- 33 | xyz : list of float 34 | The coordinates of the restrained node. 35 | direction : list of float, optional 36 | The direction of the normal. Default is [0, 0, 1]. 37 | scale : float, optional 38 | The scale factor to apply when drawing. Default is 1. 39 | """ 40 | 41 | def __init__(self, xyz, direction, scale): 42 | self.x, self.y, self.z = xyz 43 | self.direction = direction 44 | self.scale = scale 45 | 46 | 47 | class PinBCShape(_BCShape): 48 | """Pin support shape. It is a cone with base diameter and height equal to 49 | 400 units. 50 | 51 | Parameters 52 | ---------- 53 | xyz : list of float 54 | The coordinates of the restrained node. 55 | direction : list of float, optional 56 | The direction of the normal. Default is [0, 0, 1]. 57 | scale : float, optional 58 | The scale factor to apply when drawing. Default is 1. 59 | """ 60 | 61 | def __init__(self, xyz, direction=[0, 0, 1], scale=1): 62 | super(PinBCShape, self).__init__(xyz, direction, scale) 63 | self.height = 400 * self.scale 64 | self.diameter = 400 * self.scale 65 | # FIXME this is wrong because it should follow the normal 66 | self.plane = Plane([self.x, self.y, self.z - self.height], direction) 67 | self.circle = Circle(frame=Frame.from_plane(self.plane), radius=self.diameter / 2) 68 | self.shape = Cone(radius=self.circle.radius, height=self.height, frame=Frame.from_plane(self.plane)) 69 | 70 | 71 | class FixBCShape(_BCShape): 72 | """Fix support shape. It is a box with height equal to 800 units 73 | 400 units. 74 | 75 | Parameters 76 | ---------- 77 | xyz : list of float 78 | The coordinates of the restrained node. 79 | scale : float, optional 80 | The scale factor to apply when drawing. Default is 1. 81 | """ 82 | 83 | def __init__(self, xyz, scale=1): 84 | super(FixBCShape, self).__init__(xyz, [0, 0, 1], scale) 85 | self.height = 800 * self.scale 86 | f = Frame([self.x, self.y, self.z - self.height / 4], [1, 0, 0], [0, 1, 0]) 87 | self.shape = Box(self.height, self.height, self.height / 2, f) 88 | 89 | 90 | # FIXME: orient according to the direction of the restrain 91 | class RollerBCShape(_BCShape): 92 | """Roller support shape. It is a cylinder with height equal to 800 units 93 | 400 units. 94 | 95 | Parameters 96 | ---------- 97 | xyz : list of float 98 | The coordinates of the restrained node. 99 | direction : list of float, optional 100 | The direction of the normal. Default is [1, 0, 0]. 101 | scale : float, optional 102 | The scale factor to apply when drawing. Default is 1. 103 | """ 104 | 105 | def __init__(self, xyz, direction=[1, 0, 0], scale=1): 106 | super(RollerBCShape, self).__init__(xyz, direction, scale) 107 | self.height = 800 * self.scale 108 | p = Plane([self.x, self.y, self.z / 2], [0, 1, 0]) 109 | c = Circle(plane=p, radius=self.height / 2) 110 | self.shape = Cylinder(circle=c, height=self.height) 111 | 112 | 113 | class MomentShape(_BCShape): 114 | """Moment shape representation. 115 | 116 | Parameters 117 | ---------- 118 | xyz : list of float 119 | The coordinates of the point where the moment is applied. 120 | direction : list of float 121 | The direction of the moment. 122 | scale : float 123 | The scale factor to apply when drawing. 124 | """ 125 | 126 | def __init__(self, xyz, direction=[0, 0, 1], scale=1): 127 | super(MomentShape, self).__init__(xyz, direction, scale) 128 | # Define the shape for the moment representation 129 | # This is a placeholder, you can define the actual shape as needed 130 | self.shape = None 131 | 132 | 133 | class ArrowShape(_LoadShape): 134 | """Arrow shape representation. 135 | 136 | Parameters 137 | ---------- 138 | xyz : list of float 139 | The coordinates of the base of the arrow. 140 | direction : list of float 141 | The direction in which the arrow points. 142 | scale : float 143 | The scale factor to apply when drawing. Default is 1. 144 | """ 145 | 146 | def __init__(self, anchor, vector, scale=1): 147 | super(ArrowShape, self).__init__(anchor, vector, scale) 148 | self.height = vector.length * self.scale 149 | self.radius = vector.length * 0.1 * self.scale 150 | self.plane = Plane([self.x, self.y, self.z], vector) 151 | self.cone = Cone(radius=self.radius, height=self.height, frame=Frame.from_plane(self.plane)) 152 | self.shape = self.cone 153 | -------------------------------------------------------------------------------- /src/compas_fea2/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from collections import defaultdict 3 | from dotenv import load_dotenv 4 | from compas.tolerance import Tolerance # noqa: F401 5 | 6 | 7 | __author__ = ["Francesco Ranaudo"] 8 | __copyright__ = "COMPAS Association" 9 | __license__ = "MIT License" 10 | __email__ = "francesco.ranaudo@gmail.com" 11 | __version__ = "0.3.1" 12 | 13 | 14 | def init_fea2(verbose=False, point_overlap=True, global_tolerance=1, precision=3, part_nodes_limit=100000): 15 | """Create a default environment file if it doesn't exist and loads its variables. 16 | 17 | Parameters 18 | ---------- 19 | verbose : bool, optional 20 | Be verbose when printing output, by default False 21 | point_overlap : bool, optional 22 | Allow two nodes to be at the same location, by default True 23 | global_tolerance : int, optional 24 | Tolerance for the model, by default 1 25 | precision : str, optional 26 | Values approximation, by default '3'. 27 | See `compas.tolerance.Tolerance.precision` for more information. 28 | part_nodes_limit : int, optional 29 | Limit of nodes for a part, by default 100000. 30 | """ 31 | env_path = os.path.abspath(os.path.join(HERE, ".env")) 32 | if not os.path.exists(env_path): 33 | with open(env_path, "x") as f: 34 | f.write( 35 | "\n".join( 36 | [ 37 | "VERBOSE={}".format(verbose), 38 | "POINT_OVERLAP={}".format(point_overlap), 39 | "GLOBAL_TOLERANCE={}".format(global_tolerance), 40 | "PRECISION={}".format(precision), 41 | ] 42 | ) 43 | ) 44 | load_dotenv(env_path) 45 | 46 | 47 | # pluggable function to be 48 | def _register_backend(): 49 | """Create the class registry for the plugin. 50 | 51 | Raises 52 | ------ 53 | NotImplementedError 54 | This function is implemented within the backend plugin implementation. 55 | """ 56 | raise NotImplementedError 57 | 58 | 59 | def set_backend(plugin): 60 | """Set the backend plugin to be used. 61 | 62 | Parameters 63 | ---------- 64 | plugin : str 65 | Name of the plugin library. You can find some backend plugins on the 66 | official ``compas_fea2`` website. 67 | 68 | Raises 69 | ------ 70 | ImportError 71 | If the plugin library is not found. 72 | """ 73 | import importlib 74 | 75 | global BACKEND 76 | BACKEND = plugin 77 | try: 78 | importlib.import_module(plugin)._register_backend() 79 | except ImportError: 80 | print("backend plugin not found. Make sure that you have installed it before.") 81 | 82 | 83 | def _get_backend_implementation(cls): 84 | return BACKENDS[BACKEND].get(cls) 85 | 86 | 87 | HERE = os.path.dirname(__file__) 88 | 89 | HOME = os.path.abspath(os.path.join(HERE, "../../")) 90 | DATA = os.path.abspath(os.path.join(HOME, "data")) 91 | UMAT = os.path.abspath(os.path.join(DATA, "umat")) 92 | DOCS = os.path.abspath(os.path.join(HOME, "docs")) 93 | TEMP = os.path.abspath(os.path.join(HOME, "temp")) 94 | 95 | if not load_dotenv(): 96 | init_fea2() 97 | 98 | VERBOSE = os.getenv("VERBOSE").lower() == "true" 99 | POINT_OVERLAP = os.getenv("POINT_OVERLAP").lower() == "true" 100 | GLOBAL_TOLERANCE = float(os.getenv("GLOBAL_TOLERANCE")) 101 | PRECISION = int(os.getenv("PRECISION")) 102 | BACKEND = None 103 | BACKENDS = defaultdict(dict) 104 | 105 | __all__ = ["HOME", "DATA", "DOCS", "TEMP"] 106 | -------------------------------------------------------------------------------- /src/compas_fea2/base.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import json 3 | import uuid 4 | from abc import abstractmethod 5 | from copy import deepcopy 6 | from typing import Iterable 7 | 8 | import h5py 9 | import numpy as np 10 | from compas.data import Data 11 | 12 | import compas_fea2 13 | 14 | from .utilities._utils import to_dimensionless 15 | 16 | 17 | class DimensionlessMeta(type): 18 | """Metaclass for converting pint Quantity objects to dimensionless.""" 19 | 20 | def __new__(meta, name, bases, class_dict): 21 | # Decorate each method 22 | for attributeName, attribute in class_dict.items(): 23 | if callable(attribute) or isinstance(attribute, (classmethod, staticmethod)): 24 | # Unwrap classmethod/staticmethod to decorate the underlying function 25 | if isinstance(attribute, (classmethod, staticmethod)): 26 | original_func = attribute.__func__ 27 | decorated_func = to_dimensionless(original_func) 28 | # Re-wrap classmethod/staticmethod 29 | attribute = type(attribute)(decorated_func) 30 | else: 31 | attribute = to_dimensionless(attribute) 32 | class_dict[attributeName] = attribute 33 | return type.__new__(meta, name, bases, class_dict) 34 | 35 | 36 | class FEAData(Data, metaclass=DimensionlessMeta): 37 | """Base class for all FEA model objects. 38 | 39 | This base class inherits the serialisation infrastructure 40 | from the base class for core COMPAS objects: :class:`compas.base.`. 41 | 42 | It adds the abstract functionality for the representation of FEA objects 43 | in a model and/or problem summary, 44 | and for their representation in software-specific calculation files. 45 | 46 | Parameters 47 | ---------- 48 | name : str, optional 49 | The name of the object, by default None. If not provided, one is automatically 50 | generated. 51 | 52 | Attributes 53 | ---------- 54 | name : str 55 | The name of the object. 56 | registration : compas_fea2 object 57 | The mother object where this object is registered to. 58 | 59 | """ 60 | 61 | def __new__(cls, *args, **kwargs): 62 | """Try to get the backend plug-in implementation, otherwise use the base 63 | one. 64 | """ 65 | imp = compas_fea2._get_backend_implementation(cls) 66 | if not imp: 67 | return super(FEAData, cls).__new__(cls) 68 | return super(FEAData, imp).__new__(imp) 69 | 70 | def __init__(self, name=None, **kwargs): 71 | self.uid = uuid.uuid4() 72 | super().__init__() 73 | self._name = name or "".join([c for c in type(self).__name__ if c.isupper()]) + "_" + str(id(self)) 74 | self._registration = None 75 | self._key = None 76 | 77 | @property 78 | def key(self): 79 | return self._key 80 | 81 | def __repr__(self): 82 | return "{0}({1})".format(self.__class__.__name__, id(self)) 83 | 84 | def __str__(self): 85 | title = "compas_fea2 {0} object".format(self.__class__.__name__) 86 | separator = "-" * (len(title)) 87 | data_extended = [] 88 | for a in list(filter(lambda a: not a.startswith("__") and not a.startswith("_") and a != "jsondefinitions", dir(self))): 89 | try: 90 | attr = getattr(self, a) 91 | if not callable(attr): 92 | if not isinstance(attr, Iterable): 93 | data_extended.append("{0:<15} : {1}".format(a, attr.__repr__())) 94 | else: 95 | data_extended.append("{0:<15} : {1}".format(a, len(attr))) 96 | except Exception: 97 | pass 98 | return """\n{}\n{}\n{}\n""".format(title, separator, "\n".join(data_extended)) 99 | 100 | def __getstate__(self): 101 | return self.__dict__ 102 | 103 | def __setstate__(self, state): 104 | self.__dict__.update(state) 105 | 106 | @abstractmethod 107 | def jobdata(self, *args, **kwargs): 108 | """Generate the job data for the backend-specific input file.""" 109 | raise NotImplementedError("This function is not available in the selected plugin.") 110 | 111 | @classmethod 112 | def from_name(cls, name, **kwargs): 113 | """Create an instance of a class of the registered plugin from its name. 114 | 115 | Parameters 116 | ---------- 117 | name : str 118 | The name of the class (without the `_` prefix) 119 | 120 | Returns 121 | ------- 122 | obj 123 | The wanted object 124 | 125 | Notes 126 | ----- 127 | By convention, only hidden class can be called by this method. 128 | 129 | """ 130 | obj = cls(**kwargs) 131 | module_info = obj.__module__.split(".") 132 | obj = getattr(importlib.import_module(".".join([*module_info[:-1]])), "_" + name) 133 | return obj(**kwargs) 134 | 135 | # ========================================================================== 136 | # Copy and Serialization 137 | # ========================================================================== 138 | 139 | def copy(self, cls=None, copy_guid=False, copy_name=False): 140 | """Make an independent copy of the data object. 141 | 142 | Parameters 143 | ---------- 144 | cls : Type[:class:`compas.data.Data`], optional 145 | The type of data object to return. 146 | Defaults to the type of the current data object. 147 | copy_guid : bool, optional 148 | If True, the copy will have the same guid as the original. 149 | 150 | Returns 151 | ------- 152 | :class:`compas.data.Data` 153 | An independent copy of this object. 154 | 155 | """ 156 | if not cls: 157 | cls = type(self) 158 | obj = cls.__from_data__(deepcopy(self.__data__)) 159 | if copy_name and self._name is not None: 160 | obj._name = self.name 161 | if copy_guid: 162 | obj._guid = self.guid 163 | return obj # type: ignore 164 | 165 | def to_hdf5(self, hdf5_path, group_name, mode="w"): 166 | """ 167 | Save the object to an HDF5 file using the __data__ property. 168 | """ 169 | with h5py.File(hdf5_path, mode) as hdf5_file: # "a" mode to append data 170 | group = hdf5_file.require_group(f"{group_name}/{self.uid}") # Create a group for this object 171 | 172 | for key, value in self.to_hdf5_data().items(): 173 | if isinstance(value, (list, np.ndarray)): 174 | group.create_dataset(key, data=value) 175 | else: 176 | group.attrs[key] = json.dumps(value) 177 | 178 | @classmethod 179 | def from_hdf5( 180 | cls, 181 | hdf5_path, 182 | group_name, 183 | uid, 184 | ): 185 | """ 186 | Load an object from an HDF5 file using the __data__ property. 187 | """ 188 | with h5py.File(hdf5_path, "r") as hdf5_file: 189 | group = hdf5_file[f"{group_name}/{uid}"] 190 | data = {} 191 | 192 | # Load datasets (numerical values) 193 | for key in group.keys(): 194 | dataset = group[key][:] 195 | data[key] = dataset.tolist() if dataset.shape != () else dataset.item() 196 | 197 | # Load attributes (strings, dictionaries, JSON lists) 198 | for key, value in group.attrs.items(): 199 | if isinstance(value, str): 200 | # Convert "None" back to NoneType 201 | if value == "None": 202 | data[key] = None 203 | # Convert JSON back to Python objects 204 | elif value.startswith("[") or value.startswith("{"): 205 | try: 206 | data[key] = json.loads(value) 207 | except json.JSONDecodeError: 208 | data[key] = value # Keep it as a string if JSON parsing fails 209 | else: 210 | data[key] = value 211 | else: 212 | data[key] = value 213 | 214 | if not hasattr(cls, "__from_data__"): 215 | raise NotImplementedError(f"{cls.__name__} does not implement the '__from_data__' method.") 216 | return cls.__from_data__(data) 217 | 218 | def to_json(self, filepath, pretty=False, compact=False, minimal=False): 219 | """Convert an object to its native data representation and save it to a JSON file. 220 | 221 | Parameters 222 | ---------- 223 | filepath : str 224 | The path to the JSON file. 225 | pretty : bool, optional 226 | If True, format the output with newlines and indentation. 227 | compact : bool, optional 228 | If True, format the output without any whitespace. 229 | minimal : bool, optional 230 | If True, exclude the GUID from the JSON output. 231 | 232 | """ 233 | json.dump(self.__data__, open(filepath, "w"), indent=4) 234 | -------------------------------------------------------------------------------- /src/compas_fea2/cli.py: -------------------------------------------------------------------------------- 1 | """ 2 | Console script for compas_fea2. 3 | """ 4 | 5 | import importlib 6 | import os 7 | import sys 8 | 9 | import click 10 | import dotenv 11 | 12 | from compas_fea2 import HOME 13 | from compas_fea2 import VERBOSE 14 | 15 | try: 16 | from fea2_extension.main import init_plugin # type: ignore 17 | except Exception: 18 | if VERBOSE: 19 | print("WARNING: fea2_extension module not installed.") 20 | 21 | 22 | # -------------------------------- MAIN ----------------------------------# 23 | @click.group() 24 | def main(): 25 | """fea2 main. 26 | 27 | Run `fea2 ono-o-one` for more info. 28 | """ 29 | pass 30 | 31 | 32 | @main.command() 33 | def one_o_one(): 34 | """Basic explanation of command line usage.""" 35 | 36 | click.echo("\nHey there! this is the command line interface (CLI) for compas_fea2!\nWIP") 37 | 38 | 39 | @main.command() 40 | @click.option("--clean", default="False", help="remove existing directories") 41 | @click.argument("backend") 42 | def init_backend(backend, clean): 43 | """Initialize a bare backend module.\n 44 | backend : txt\n 45 | The name of the backend. This is must be lower case. 46 | """ 47 | init_plugin(HOME, backend, clean) 48 | backend = backend.lower() 49 | 50 | 51 | @main.command() 52 | @click.argument("backend") 53 | @click.argument("setting") 54 | @click.argument("value") 55 | def change_setting(backend, setting, value): 56 | """Change a setting for the specified backend.\n 57 | backend : txt\n 58 | The name of the backend. 59 | setting : txt\n 60 | The setting to be changed. 61 | value : txt\n 62 | The new value for the setting. 63 | 64 | Example usage:\n 65 | fea2 change-setting opensees exe "Applications/OpenSees3.5.0/bin/OpenSees" 66 | """ 67 | m = importlib.import_module("compas_fea2_" + backend.lower()) 68 | env = os.path.join(m.HOME, "src", "compas_fea2_" + backend.lower(), ".env") 69 | dotenv.set_key(env, setting.upper(), value) 70 | print(f"{setting.upper()} set to {value} for compas_fea2_{backend.lower()}") 71 | 72 | 73 | # -------------------------------- DEBUG ----------------------------------# 74 | if __name__ == "__main__": 75 | sys.exit(main.init_backend()) 76 | -------------------------------------------------------------------------------- /src/compas_fea2/job/__init__.py: -------------------------------------------------------------------------------- 1 | from .input_file import InputFile 2 | from .input_file import ParametersFile 3 | 4 | __all__ = ["InputFile", "ParametersFile"] 5 | -------------------------------------------------------------------------------- /src/compas_fea2/job/input_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from compas_fea2 import VERBOSE 4 | from compas_fea2.base import FEAData 5 | 6 | 7 | class InputFile(FEAData): 8 | """Input file object for standard FEA. 9 | 10 | Parameters 11 | ---------- 12 | name : str, optional 13 | Unique identifier. If not provided, it is automatically generated. Set a 14 | name if you want a more human-readable input file. 15 | 16 | Attributes 17 | ---------- 18 | name : str 19 | Unique identifier. 20 | problem : :class:`compas_fea2.problem.Problem` 21 | The problem to generate the input file from. 22 | model : :class:`compas_fea2.model.Model` 23 | The model associated with the problem. 24 | path : str 25 | Complete path to the input file. 26 | 27 | """ 28 | 29 | def __init__(self, problem, **kwargs): 30 | super().__init__(**kwargs) 31 | self._registration = problem 32 | self._extension = None 33 | self.path = None 34 | 35 | @property 36 | def file_name(self): 37 | return "{}.{}".format(self.problem._name, self._extension) 38 | 39 | @property 40 | def problem(self): 41 | return self._registration 42 | 43 | @property 44 | def model(self): 45 | return self.problem._registration 46 | 47 | # ============================================================================== 48 | # General methods 49 | # ============================================================================== 50 | 51 | def write_to_file(self, path=None): 52 | """Writes the InputFile to a file in a specified location. 53 | 54 | Parameters 55 | ---------- 56 | path : str, optional 57 | Path to the folder where the input file will be saved, by default 58 | ``None``. If not provided, the Problem path attributed is used. 59 | 60 | Returns 61 | ------- 62 | str 63 | Information about the results of the writing process. 64 | 65 | """ 66 | path = path or self.problem.path 67 | if not path: 68 | raise ValueError("A path to the folder for the input file must be provided") 69 | file_path = os.path.join(path, self.file_name) 70 | with open(file_path, "w") as f: 71 | f.writelines(self.jobdata()) 72 | if VERBOSE: 73 | print("Input file generated in the following location: {}".format(file_path)) 74 | 75 | 76 | class ParametersFile(InputFile): 77 | """Input file object for Optimizations.""" 78 | 79 | def __init__(self, **kwargs): 80 | super().__init__(**kwargs) 81 | raise NotImplementedError 82 | -------------------------------------------------------------------------------- /src/compas_fea2/model/__init__.py: -------------------------------------------------------------------------------- 1 | from .model import Model 2 | from .parts import ( 3 | Part, 4 | RigidPart, 5 | ) 6 | from .nodes import Node 7 | from .elements import ( 8 | _Element, 9 | MassElement, 10 | _Element0D, 11 | SpringElement, 12 | LinkElement, 13 | _Element1D, 14 | BeamElement, 15 | TrussElement, 16 | StrutElement, 17 | TieElement, 18 | _Element2D, 19 | ShellElement, 20 | MembraneElement, 21 | _Element3D, 22 | TetrahedronElement, 23 | HexahedronElement, 24 | ) 25 | from .materials.material import ( 26 | _Material, 27 | ElasticIsotropic, 28 | ElasticOrthotropic, 29 | ElasticPlastic, 30 | Stiff, 31 | UserMaterial, 32 | ) 33 | from .materials.concrete import ( 34 | Concrete, 35 | ConcreteDamagedPlasticity, 36 | ConcreteSmearedCrack, 37 | ) 38 | from .materials.steel import Steel 39 | from .materials.timber import Timber 40 | from .sections import ( 41 | _Section, 42 | MassSection, 43 | SpringSection, 44 | ConnectorSection, 45 | BeamSection, 46 | GenericBeamSection, 47 | AngleSection, 48 | BoxSection, 49 | CircularSection, 50 | HexSection, 51 | ISection, 52 | PipeSection, 53 | RectangularSection, 54 | ShellSection, 55 | MembraneSection, 56 | SolidSection, 57 | TrapezoidalSection, 58 | TrussSection, 59 | StrutSection, 60 | TieSection, 61 | ) 62 | from .constraints import ( 63 | _Constraint, 64 | _MultiPointConstraint, 65 | TieMPC, 66 | BeamMPC, 67 | TieConstraint, 68 | ) 69 | from .connectors import ( 70 | Connector, 71 | LinearConnector, 72 | RigidLinkConnector, 73 | SpringConnector, 74 | ZeroLengthConnector, 75 | ZeroLengthSpringConnector, 76 | ZeroLengthContactConnector, 77 | ) 78 | from .groups import ( 79 | _Group, 80 | NodesGroup, 81 | ElementsGroup, 82 | FacesGroup, 83 | PartsGroup, 84 | ) 85 | from .releases import ( 86 | _BeamEndRelease, 87 | BeamEndPinRelease, 88 | BeamEndSliderRelease, 89 | ) 90 | from .bcs import ( 91 | _BoundaryCondition, 92 | GeneralBC, 93 | FixedBC, 94 | FixedBCX, 95 | FixedBCY, 96 | FixedBCZ, 97 | PinnedBC, 98 | ClampBCXX, 99 | ClampBCYY, 100 | ClampBCZZ, 101 | RollerBCX, 102 | RollerBCY, 103 | RollerBCZ, 104 | RollerBCXY, 105 | RollerBCYZ, 106 | RollerBCXZ, 107 | ) 108 | 109 | from .ics import ( 110 | _InitialCondition, 111 | InitialTemperatureField, 112 | InitialStressField, 113 | ) 114 | 115 | from .interfaces import ( 116 | Interface, 117 | ) 118 | 119 | from .interactions import ( 120 | _Interaction, 121 | Contact, 122 | HardContactFrictionPenalty, 123 | HardContactNoFriction, 124 | LinearContactFrictionPenalty, 125 | HardContactRough, 126 | ) 127 | 128 | __all__ = [ 129 | "Model", 130 | "Part", 131 | "RigidPart", 132 | "Node", 133 | "_Element", 134 | "MassElement", 135 | "_Element0D", 136 | "LinkElement", 137 | "_Element1D", 138 | "BeamElement", 139 | "SpringElement", 140 | "TrussElement", 141 | "StrutElement", 142 | "TieElement", 143 | "_Element2D", 144 | "ShellElement", 145 | "MembraneElement", 146 | "_Element3D", 147 | "TetrahedronElement", 148 | "HexahedronElement", 149 | "_Material", 150 | "UserMaterial", 151 | "Concrete", 152 | "ConcreteSmearedCrack", 153 | "ConcreteDamagedPlasticity", 154 | "ElasticIsotropic", 155 | "Stiff", 156 | "ElasticOrthotropic", 157 | "ElasticPlastic", 158 | "Steel", 159 | "Timber", 160 | "_Section", 161 | "MassSection", 162 | "ConnectorSection", 163 | "BeamSection", 164 | "GenericBeamSection", 165 | "SpringSection", 166 | "AngleSection", 167 | "BoxSection", 168 | "CircularSection", 169 | "HexSection", 170 | "ISection", 171 | "PipeSection", 172 | "RectangularSection", 173 | "ShellSection", 174 | "MembraneSection", 175 | "SolidSection", 176 | "TrapezoidalSection", 177 | "TrussSection", 178 | "StrutSection", 179 | "TieSection", 180 | "_Constraint", 181 | "_MultiPointConstraint", 182 | "TieMPC", 183 | "BeamMPC", 184 | "TieConstraint", 185 | "_BeamEndRelease", 186 | "BeamEndPinRelease", 187 | "BeamEndSliderRelease", 188 | "_Group", 189 | "NodesGroup", 190 | "ElementsGroup", 191 | "FacesGroup", 192 | "PartsGroup", 193 | "_BoundaryCondition", 194 | "GeneralBC", 195 | "FixedBCX", 196 | "FixedBCY", 197 | "FixedBCZ", 198 | "FixedBC", 199 | "PinnedBC", 200 | "ClampBCXX", 201 | "ClampBCYY", 202 | "ClampBCZZ", 203 | "RollerBCX", 204 | "RollerBCY", 205 | "RollerBCZ", 206 | "RollerBCXY", 207 | "RollerBCYZ", 208 | "RollerBCXZ", 209 | "_InitialCondition", 210 | "InitialTemperatureField", 211 | "InitialStressField", 212 | "Interface", 213 | "Connector", 214 | "LinearConnector", 215 | "SpringConnector", 216 | "RigidLinkConnector", 217 | "ZeroLengthConnector", 218 | "ZeroLengthContactConnector", 219 | "ZeroLengthSpringConnector", 220 | "_Interaction", 221 | "Contact", 222 | "HardContactFrictionPenalty", 223 | "HardContactNoFriction", 224 | "LinearContactFrictionPenalty", 225 | "HardContactRough", 226 | ] 227 | -------------------------------------------------------------------------------- /src/compas_fea2/model/bcs.py: -------------------------------------------------------------------------------- 1 | from typing import Dict 2 | 3 | from compas_fea2.base import FEAData 4 | 5 | docs = """ 6 | Note 7 | ---- 8 | BoundaryConditions are registered to a :class:`compas_fea2.model.Model`. 9 | 10 | Warning 11 | ------- 12 | The `axes` parameter is WIP. Currently only global axes can be used. 13 | 14 | Parameters 15 | ---------- 16 | name : str, optional 17 | Unique identifier. If not provided it is automatically generated. Set a 18 | name if you want a more human-readable input file. 19 | axes : str, optional 20 | The reference axes. 21 | 22 | Attributes 23 | ---------- 24 | name : str 25 | Unique identifier. 26 | x : bool 27 | Restrain translations along the x axis. 28 | y : bool 29 | Restrain translations along the y axis. 30 | z : bool 31 | Restrain translations along the z axis. 32 | xx : bool 33 | Restrain rotations around the x axis. 34 | yy : bool 35 | Restrain rotations around the y axis. 36 | zz : bool 37 | Restrain rotations around the z axis. 38 | components : dict 39 | Dictionary with component-value pairs summarizing the boundary condition. 40 | axes : str 41 | The reference axes. 42 | """ 43 | 44 | 45 | class _BoundaryCondition(FEAData): 46 | """Base class for all zero-valued boundary conditions.""" 47 | 48 | __doc__ += docs # type: ignore 49 | 50 | def __init__(self, axes: str = "global", **kwargs): 51 | super().__init__(**kwargs) 52 | self._axes = axes 53 | self._x = False 54 | self._y = False 55 | self._z = False 56 | self._xx = False 57 | self._yy = False 58 | self._zz = False 59 | 60 | @property 61 | def x(self) -> bool: 62 | return self._x 63 | 64 | @property 65 | def y(self) -> bool: 66 | return self._y 67 | 68 | @property 69 | def z(self) -> bool: 70 | return self._z 71 | 72 | @property 73 | def xx(self) -> bool: 74 | return self._xx 75 | 76 | @property 77 | def yy(self) -> bool: 78 | return self._yy 79 | 80 | @property 81 | def zz(self) -> bool: 82 | return self._zz 83 | 84 | @property 85 | def axes(self) -> str: 86 | return self._axes 87 | 88 | @axes.setter 89 | def axes(self, value: str): 90 | self._axes = value 91 | 92 | @property 93 | def components(self) -> Dict[str, bool]: 94 | return {c: getattr(self, c) for c in ["x", "y", "z", "xx", "yy", "zz"]} 95 | 96 | @property 97 | def __data__(self) -> dict: 98 | return { 99 | "class": self.__class__.__base__.__name__, 100 | "axes": self._axes, 101 | "x": self._x, 102 | "y": self._y, 103 | "z": self._z, 104 | "xx": self._xx, 105 | "yy": self._yy, 106 | "zz": self._zz, 107 | } 108 | 109 | @classmethod 110 | def __from_data__(cls, data: dict): 111 | return cls( 112 | axes=data.get("axes", "global"), 113 | x=data.get("x", False), 114 | y=data.get("y", False), 115 | z=data.get("z", False), 116 | xx=data.get("xx", False), 117 | yy=data.get("yy", False), 118 | zz=data.get("zz", False), 119 | ) 120 | 121 | 122 | class GeneralBC(_BoundaryCondition): 123 | """Customized boundary condition.""" 124 | 125 | __doc__ += docs # type: ignore 126 | 127 | __doc__ += """ 128 | Additional Parameters 129 | --------------------- 130 | x : bool 131 | Restrain translations along the x axis. 132 | y : bool 133 | Restrain translations along the y axis. 134 | z : bool 135 | Restrain translations along the z axis. 136 | xx : bool 137 | Restrain rotations around the x axis. 138 | yy : bool 139 | Restrain rotations around the y axis. 140 | zz : bool 141 | Restrain rotations around the z axis. 142 | """ 143 | 144 | def __init__(self, x: bool = False, y: bool = False, z: bool = False, xx: bool = False, yy: bool = False, zz: bool = False, **kwargs): 145 | super().__init__(**kwargs) 146 | self._x = x 147 | self._y = y 148 | self._z = z 149 | self._xx = xx 150 | self._yy = yy 151 | self._zz = zz 152 | 153 | 154 | class FixedBC(_BoundaryCondition): 155 | """A fixed nodal displacement boundary condition.""" 156 | 157 | __doc__ += docs # type: ignore 158 | 159 | def __init__(self, **kwargs): 160 | super().__init__(**kwargs) 161 | self._x = True 162 | self._y = True 163 | self._z = True 164 | self._xx = True 165 | self._yy = True 166 | self._zz = True 167 | 168 | 169 | class FixedBCX(_BoundaryCondition): 170 | """A fixed nodal displacement boundary condition along and around X.""" 171 | 172 | __doc__ += docs # type: ignore 173 | 174 | def __init__(self, **kwargs): 175 | super().__init__(**kwargs) 176 | self._x = True 177 | self._xx = True 178 | 179 | 180 | class FixedBCY(_BoundaryCondition): 181 | """A fixed nodal displacement boundary condition along and around Y.""" 182 | 183 | __doc__ += docs # type: ignore 184 | 185 | def __init__(self, **kwargs): 186 | super().__init__(**kwargs) 187 | self._y = True 188 | self._yy = True 189 | 190 | 191 | class FixedBCZ(_BoundaryCondition): 192 | """A fixed nodal displacement boundary condition along and around Z.""" 193 | 194 | __doc__ += docs # type: ignore 195 | 196 | def __init__(self, **kwargs): 197 | super().__init__(**kwargs) 198 | self._z = True 199 | self._zz = True 200 | 201 | 202 | class PinnedBC(_BoundaryCondition): 203 | """A pinned nodal displacement boundary condition.""" 204 | 205 | __doc__ += docs # type: ignore 206 | 207 | def __init__(self, **kwargs): 208 | super().__init__(**kwargs) 209 | self._x = True 210 | self._y = True 211 | self._z = True 212 | 213 | 214 | class ClampBCXX(PinnedBC): 215 | """A pinned nodal displacement boundary condition clamped in XX.""" 216 | 217 | __doc__ += docs # type: ignore 218 | 219 | def __init__(self, **kwargs): 220 | super().__init__(**kwargs) 221 | self._xx = True 222 | 223 | 224 | class ClampBCYY(PinnedBC): 225 | """A pinned nodal displacement boundary condition clamped in YY.""" 226 | 227 | __doc__ += docs # type: ignore 228 | 229 | def __init__(self, **kwargs): 230 | super().__init__(**kwargs) 231 | self._yy = True 232 | 233 | 234 | class ClampBCZZ(PinnedBC): 235 | """A pinned nodal displacement boundary condition clamped in ZZ.""" 236 | 237 | __doc__ += docs # type: ignore 238 | 239 | def __init__(self, **kwargs): 240 | super().__init__(**kwargs) 241 | self._zz = True 242 | 243 | 244 | class RollerBCX(PinnedBC): 245 | """A pinned nodal displacement boundary condition released in X.""" 246 | 247 | __doc__ += docs # type: ignore 248 | 249 | def __init__(self, **kwargs): 250 | super().__init__(**kwargs) 251 | self._x = False 252 | 253 | 254 | class RollerBCY(PinnedBC): 255 | """A pinned nodal displacement boundary condition released in Y.""" 256 | 257 | __doc__ += docs # type: ignore 258 | 259 | def __init__(self, **kwargs): 260 | super().__init__(**kwargs) 261 | self._y = False 262 | 263 | 264 | class RollerBCZ(PinnedBC): 265 | """A pinned nodal displacement boundary condition released in Z.""" 266 | 267 | __doc__ += docs # type: ignore 268 | 269 | def __init__(self, **kwargs): 270 | super().__init__(**kwargs) 271 | self._z = False 272 | 273 | 274 | class RollerBCXY(PinnedBC): 275 | """A pinned nodal displacement boundary condition released in X and Y.""" 276 | 277 | __doc__ += docs # type: ignore 278 | 279 | def __init__(self, **kwargs): 280 | super().__init__(**kwargs) 281 | self._x = False 282 | self._y = False 283 | 284 | 285 | class RollerBCYZ(PinnedBC): 286 | """A pinned nodal displacement boundary condition released in Y and Z.""" 287 | 288 | __doc__ += docs # type: ignore 289 | 290 | def __init__(self, **kwargs): 291 | super().__init__(**kwargs) 292 | self._y = False 293 | self._z = False 294 | 295 | 296 | class RollerBCXZ(PinnedBC): 297 | """A pinned nodal displacement boundary condition released in X and Z.""" 298 | 299 | __doc__ += docs # type: ignore 300 | 301 | def __init__(self, **kwargs): 302 | super().__init__(**kwargs) 303 | self._x = False 304 | self._z = False 305 | -------------------------------------------------------------------------------- /src/compas_fea2/model/constraints.py: -------------------------------------------------------------------------------- 1 | from compas_fea2.base import FEAData 2 | 3 | 4 | class _Constraint(FEAData): 5 | """Base class for constraints. 6 | 7 | A constraint removes degree of freedom of nodes in the model. 8 | """ 9 | 10 | def __init__(self, **kwargs) -> None: 11 | super().__init__(**kwargs) 12 | 13 | @property 14 | def __data__(self): 15 | return { 16 | "class": self.__class__.__base__.__name__, 17 | } 18 | 19 | @classmethod 20 | def __from_data__(cls, data): 21 | return cls(**data) 22 | 23 | 24 | # ------------------------------------------------------------------------------ 25 | # MPC 26 | # ------------------------------------------------------------------------------ 27 | 28 | 29 | class _MultiPointConstraint(_Constraint): 30 | """A MultiPointConstraint (MPC) links a node (master) to other nodes (slaves) in the model. 31 | 32 | Parameters 33 | ---------- 34 | constraint_type : str 35 | Type of the constraint. 36 | master : :class:`compas_fea2.model.Node` 37 | Node that acts as master. 38 | slaves : List[:class:`compas_fea2.model.Node`] | :class:`compas_fea2.model.NodesGroup` 39 | List or Group of nodes that act as slaves. 40 | tol : float 41 | Constraint tolerance, distance limit between master and slaves. 42 | 43 | Attributes 44 | ---------- 45 | constraint_type : str 46 | Type of the constraint. 47 | master : :class:`compas_fea2.model.Node` 48 | Node that acts as master. 49 | slaves : List[:class:`compas_fea2.model.Node`] | :class:`compas_fea2.model.NodesGroup` 50 | List or Group of nodes that act as slaves. 51 | tol : float 52 | Constraint tolerance, distance limit between master and slaves. 53 | 54 | Notes 55 | ----- 56 | Constraints are registered to a :class:`compas_fea2.model.Model`. 57 | 58 | """ 59 | 60 | def __init__(self, constraint_type: str, **kwargs) -> None: 61 | super().__init__(**kwargs) 62 | self.constraint_type = constraint_type 63 | 64 | @property 65 | def __data__(self): 66 | data = super().__data__ 67 | data.update( 68 | { 69 | "constraint_type": self.constraint_type, 70 | # ...other attributes... 71 | } 72 | ) 73 | return data 74 | 75 | @classmethod 76 | def __from_data__(cls, data): 77 | return cls(constraint_type=data["constraint_type"], **data) 78 | 79 | 80 | class TieMPC(_MultiPointConstraint): 81 | """Tie MPC that constraints axial translations.""" 82 | 83 | 84 | class BeamMPC(_MultiPointConstraint): 85 | """Beam MPC that constraints axial translations and rotations.""" 86 | 87 | 88 | # TODO check! 89 | class _SurfaceConstraint(_Constraint): 90 | """A SurfaceConstraint links a surface (master) to another surface (slave) in the model. 91 | 92 | Parameters 93 | ---------- 94 | master : :class:`compas_fea2.model.Node` 95 | Node that acts as master. 96 | slaves : List[:class:`compas_fea2.model.Node`] | :class:`compas_fea2.model.NodesGroup` 97 | List or Group of nodes that act as slaves. 98 | tol : float 99 | Constraint tolerance, distance limit between master and slaves. 100 | 101 | Attributes 102 | ---------- 103 | master : :class:`compas_fea2.model.Node` 104 | Node that acts as master. 105 | slaves : List[:class:`compas_fea2.model.Node`] | :class:`compas_fea2.model.NodesGroup` 106 | List or Group of nodes that act as slaves. 107 | tol : float 108 | Constraint tolerance, distance limit between master and slaves. 109 | 110 | """ 111 | 112 | @property 113 | def __data__(self): 114 | data = super().__data__ 115 | # ...update data with specific attributes... 116 | return data 117 | 118 | @classmethod 119 | def __from_data__(cls, data): 120 | return cls(**data) 121 | 122 | 123 | class TieConstraint(_SurfaceConstraint): 124 | """Tie constraint between two surfaces.""" 125 | -------------------------------------------------------------------------------- /src/compas_fea2/model/ics.py: -------------------------------------------------------------------------------- 1 | from compas_fea2.base import FEAData 2 | 3 | 4 | class _InitialCondition(FEAData): 5 | """Base class for all predefined initial conditions. 6 | 7 | Notes 8 | ----- 9 | InitialConditions are registered to a :class:`compas_fea2.model.Model`. The 10 | same InitialCondition can be assigned to Nodes or Elements in multiple Parts 11 | 12 | """ 13 | 14 | def __init__(self, **kwargs): 15 | super().__init__(**kwargs) 16 | 17 | @property 18 | def __data__(self) -> dict: 19 | return { 20 | "type": self.__class__.__base__.__name__, 21 | } 22 | 23 | @classmethod 24 | def __from_data__(cls, data): 25 | return cls(**data) 26 | 27 | 28 | # FIXME this is not really a field in the sense that it is only applied to 1 node/element 29 | class InitialTemperatureField(_InitialCondition): 30 | """Temperature field. 31 | 32 | Parameters 33 | ---------- 34 | temperature : float 35 | The temperature value. 36 | 37 | Attributes 38 | ---------- 39 | temperature : float 40 | The temperature value. 41 | 42 | Notes 43 | ----- 44 | InitialConditions are registered to a :class:`compas_fea2.model.Model`. The 45 | same InitialCondition can be assigned to Nodes or Elements in multiple Parts 46 | 47 | """ 48 | 49 | def __init__(self, temperature, **kwargs): 50 | super().__init__(**kwargs) 51 | self._t = temperature 52 | 53 | @property 54 | def temperature(self): 55 | return self._t 56 | 57 | @temperature.setter 58 | def temperature(self, value): 59 | self._t = value 60 | 61 | @property 62 | def __data__(self): 63 | data = super().__data__ 64 | data.update( 65 | { 66 | "temperature": self._t, 67 | } 68 | ) 69 | return data 70 | 71 | @classmethod 72 | def __from_data__(cls, data): 73 | temperature = data.pop("temperature") 74 | return cls(temperature, **data) 75 | 76 | 77 | class InitialStressField(_InitialCondition): 78 | """Stress field. 79 | 80 | Parameters 81 | ---------- 82 | stress : touple(float, float, float) 83 | The stress values. 84 | 85 | Attributes 86 | ---------- 87 | stress : touple(float, float, float) 88 | The stress values. 89 | 90 | Notes 91 | ----- 92 | InitialConditions are registered to a :class:`compas_fea2.model.Model` 93 | The same InitialCondition can be assigned to Nodes or Elements in multiple Parts. 94 | 95 | """ 96 | 97 | def __init__(self, stress, **kwargs): 98 | super().__init__(**kwargs) 99 | self._s = stress 100 | 101 | @property 102 | def stress(self): 103 | return self._s 104 | 105 | @stress.setter 106 | def stress(self, value): 107 | if not isinstance(value, tuple) or len(value) != 3: 108 | raise TypeError("you must provide a tuple with 3 elements") 109 | self._s = value 110 | 111 | @property 112 | def __data__(self): 113 | data = super().__data__ 114 | data.update({"stress": self._s}) 115 | return data 116 | 117 | @classmethod 118 | def __from_data__(cls, data): 119 | stress = data.pop("stress") 120 | return cls(stress, **data) 121 | -------------------------------------------------------------------------------- /src/compas_fea2/model/interactions.py: -------------------------------------------------------------------------------- 1 | from compas_fea2.base import FEAData 2 | 3 | 4 | class _Interaction(FEAData): 5 | """Base class for all interactions.""" 6 | 7 | def __init__(self, **kwargs): 8 | super().__init__(**kwargs) 9 | 10 | 11 | # ------------------------------------------------------------------------------ 12 | # SURFACE TO SURFACE INTERACTION 13 | # ------------------------------------------------------------------------------ 14 | class Contact(_Interaction): 15 | """General contact interaction between two parts. 16 | 17 | Note 18 | ---- 19 | Interactions are registered to a :class:`compas_fea2.model.Model` and can be 20 | assigned to multiple interfaces. 21 | 22 | Parameters 23 | ---------- 24 | name : str, optional 25 | Uniqe identifier. If not provided it is automatically generated. Set a 26 | name if you want a more human-readable input file. 27 | normal : str 28 | Behaviour of the contact along the direction normal to the interaction 29 | surface. For faceted surfaces, this is the behavior along the direction 30 | normal to each face. 31 | tangent : 32 | Behaviour of the contact along the directions tangent to the interaction 33 | surface. For faceted surfaces, this is the behavior along the directions 34 | tangent to each face. 35 | 36 | Attributes 37 | ---------- 38 | name : str 39 | Uniqe identifier. If not provided it is automatically generated. Set a 40 | name if you want a more human-readable input file. 41 | normal : str 42 | Behaviour of the contact along the direction normal to the interaction 43 | surface. For faceted surfaces, this is the behavior along the direction 44 | normal to each face. 45 | tangent : 46 | Behaviour of the contact along the directions tangent to the interaction 47 | surface. For faceted surfaces, this is the behavior along the directions 48 | tangent to each face. 49 | """ 50 | 51 | def __init__(self, *, normal, tangent, **kwargs): 52 | super().__init__(**kwargs) 53 | self._tangent = tangent 54 | self._normal = normal 55 | 56 | @property 57 | def tangent(self): 58 | return self._tangent 59 | 60 | @property 61 | def normal(self): 62 | return self._normal 63 | 64 | 65 | class HardContactNoFriction(Contact): 66 | """Hard contact interaction property with friction using a penalty 67 | formulation. 68 | 69 | Parameters 70 | ---------- 71 | mu : float 72 | Friction coefficient for tangential behaviour. 73 | tollerance : float 74 | Slippage tollerance during contact. 75 | 76 | Attributes 77 | ---------- 78 | name : str 79 | Automatically generated id. You can change the name if you want a more 80 | human readable input file. 81 | mu : float 82 | Friction coefficient for tangential behaviour. 83 | tollerance : float 84 | Slippage tollerance during contact. 85 | """ 86 | 87 | def __init__(self, tol, **kwargs) -> None: 88 | super().__init__(normal="HARD", tangent=None, **kwargs) 89 | self._tol = tol 90 | 91 | 92 | class HardContactFrictionPenalty(Contact): 93 | """Hard contact interaction property with friction using a penalty 94 | formulation. 95 | 96 | Parameters 97 | ---------- 98 | mu : float 99 | Friction coefficient for tangential behaviour. 100 | tollerance : float 101 | Slippage tollerance during contact. 102 | 103 | Attributes 104 | ---------- 105 | name : str 106 | Automatically generated id. You can change the name if you want a more 107 | human readable input file. 108 | mu : float 109 | Friction coefficient for tangential behaviour. 110 | tollerance : float 111 | Slippage tollerance during contact. 112 | """ 113 | 114 | def __init__(self, mu, tol, **kwargs) -> None: 115 | super().__init__(normal="HARD", tangent=mu, **kwargs) 116 | self._tol = tol 117 | 118 | @property 119 | def mu(self): 120 | return self._tangent 121 | 122 | @property 123 | def tol(self): 124 | return self._tol 125 | 126 | @tol.setter 127 | def tol(self, value): 128 | self._tol = value 129 | 130 | 131 | class LinearContactFrictionPenalty(Contact): 132 | """Contact interaction property with linear softnening and friction using a 133 | penalty formulation. 134 | 135 | Parameters 136 | ---------- 137 | stiffness : float 138 | Stiffness of the the contact in the normal direction. 139 | mu : float 140 | Friction coefficient for tangential behaviour. 141 | tollerance : float 142 | Slippage tollerance during contact. 143 | 144 | Attributes 145 | ---------- 146 | name : str 147 | Automatically generated id. You can change the name if you want a more 148 | human readable input file. 149 | mu : float 150 | Friction coefficient for tangential behaviour. 151 | tollerance : float 152 | Slippage tollerance during contact. 153 | """ 154 | 155 | def __init__(self, *, stiffness, mu, tolerance, **kwargs) -> None: 156 | super().__init__(normal="Linear", tangent=mu, **kwargs) 157 | self._tolerance = tolerance 158 | self._stiffness = stiffness 159 | 160 | @property 161 | def stiffness(self): 162 | return self._stiffness 163 | 164 | @stiffness.setter 165 | def stiffness(self, value): 166 | self._stiffness = value 167 | 168 | @property 169 | def tolerance(self): 170 | return self._tolerance 171 | 172 | @tolerance.setter 173 | def tolerance(self, value): 174 | self._tolerance = value 175 | 176 | 177 | class HardContactRough(Contact): 178 | """Hard contact interaction property with indefinite friction (rough surfaces). 179 | 180 | Parameters 181 | ---------- 182 | name : str, optional 183 | You can change the name if you want a more human readable input file. 184 | 185 | Attributes 186 | ---------- 187 | name : str 188 | Automatically generated id. You can change the name if you want a more 189 | human readable input file. 190 | mu : float 191 | Friction coefficient for tangential behaviour. 192 | tollerance : float 193 | Slippage tollerance during contact. 194 | """ 195 | 196 | def __init__(self, **kwargs) -> None: 197 | super().__init__(normal="HARD", tangent="ROUGH", **kwargs) 198 | -------------------------------------------------------------------------------- /src/compas_fea2/model/interfaces.py: -------------------------------------------------------------------------------- 1 | from compas_fea2.base import FEAData 2 | 3 | 4 | class Interface(FEAData): 5 | """An interface is defined as a pair of master and slave surfaces 6 | with a behavior property between them. 7 | 8 | Note 9 | ---- 10 | Interfaces are registered to a :class:`compas_fea2.model.Model`. 11 | 12 | Parameters 13 | ---------- 14 | name : str, optional 15 | Uniqe identifier. If not provided it is automatically generated. Set a 16 | name if you want a more human-readable input file. 17 | master : :class:`compas_fea2.model.FacesGroup` 18 | Group of element faces determining the Master surface. 19 | slave : :class:`compas_fea2.model.FacesGroup` 20 | Group of element faces determining the Slave surface. 21 | behavior : :class:`compas_fea2.model._Interaction` 22 | behavior type between master and slave. 23 | 24 | Attributes 25 | ---------- 26 | name : str 27 | Uniqe identifier. If not provided it is automatically generated. Set a 28 | name if you want a more human-readable input file. 29 | master : :class:`compas_fea2.model.FacesGroup` 30 | Group of element faces determining the Master surface. 31 | slave : :class:`compas_fea2.model.FacesGroup` 32 | Group of element faces determining the Slave surface. 33 | behavior : :class:`compas_fea2.model._Interaction` | :class:`compas_fea2.model._Constraint` 34 | behavior type between master and slave. 35 | 36 | """ 37 | 38 | def __init__(self, master, slave, behavior, name=None, **kwargs): 39 | super().__init__(name=name, **kwargs) 40 | self._master = master 41 | self._slave = slave 42 | self._behavior = behavior 43 | 44 | @property 45 | def master(self): 46 | return self._master 47 | 48 | @property 49 | def slave(self): 50 | return self._slave 51 | 52 | @property 53 | def behavior(self): 54 | return self._behavior 55 | 56 | 57 | # class ContactInterface(Interface): 58 | # """Interface for contact behavior 59 | 60 | # Parameters 61 | # ---------- 62 | # Interface : _type_ 63 | # _description_ 64 | # """ 65 | # def __init__(self, master, slave, behavior, name=None, **kwargs): 66 | # super().__init__(master, slave, behavior, name, **kwargs) 67 | 68 | # class ConstraintInterface(Interface): 69 | # """Interface for contact behavior 70 | 71 | # Parameters 72 | # ---------- 73 | # Interface : _type_ 74 | # _description_ 75 | # """ 76 | # def __init__(self, master, slave, behavior, name=None, **kwargs): 77 | # super().__init__(master, slave, behavior, name, **kwargs) 78 | -------------------------------------------------------------------------------- /src/compas_fea2/model/materials/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/src/compas_fea2/model/materials/__init__.py -------------------------------------------------------------------------------- /src/compas_fea2/model/materials/steel.py: -------------------------------------------------------------------------------- 1 | from compas_fea2.units import UnitRegistry 2 | from compas_fea2.units import units as u 3 | 4 | from .material import ElasticIsotropic 5 | 6 | 7 | class Steel(ElasticIsotropic): 8 | """Bi-linear steel with given yield stress. 9 | 10 | Parameters 11 | ---------- 12 | E : float 13 | Young's modulus E. 14 | v : float 15 | Poisson's ratio v. 16 | fy : float 17 | Yield stress. 18 | fu : float 19 | Ultimate stress. 20 | eu : float 21 | Ultimate strain. 22 | density : float, optional 23 | Density of the steel material [kg/m^3]. 24 | name : str, optional 25 | Name of the material. 26 | 27 | Attributes 28 | ---------- 29 | E : float 30 | Young's modulus E. 31 | v : float 32 | Poisson's ratio v. 33 | G : float 34 | Shear modulus G. 35 | fy : float 36 | Yield stress. 37 | fu : float 38 | Ultimate stress. 39 | eu : float 40 | Ultimate strain. 41 | ep : float 42 | Plastic strain. 43 | tension : dict 44 | Parameters for modelling the tension side of the stress-strain curve. 45 | compression : dict 46 | Parameters for modelling the compression side of the stress-strain curve. 47 | """ 48 | 49 | def __init__(self, *, E, v, density, fy, fu, eu, **kwargs): 50 | super().__init__(E=E, v=v, density=density, **kwargs) 51 | 52 | fu = fu or fy 53 | 54 | # E *= 10**9 55 | # fu *= 10**6 56 | # fy *= 10**6 57 | # eu *= 0.01 58 | 59 | ep = eu - fy / E 60 | f = [fy, fu] 61 | e = [0, ep] 62 | fc = [-i for i in f] 63 | ec = [-i for i in e] 64 | 65 | self.fy = fy 66 | self.fu = fu 67 | self.eu = eu 68 | self.ep = ep 69 | self.E = E 70 | self.v = v 71 | self.tension = {"f": f, "e": e} 72 | self.compression = {"f": fc, "e": ec} 73 | 74 | def __str__(self): 75 | return """ 76 | Steel Material 77 | -------------- 78 | name : {} 79 | density : {:.2f} 80 | 81 | E : {:.2f} 82 | G : {:.2f} 83 | fy : {:.2f} 84 | fu : {:.2f} 85 | v : {:.2f} 86 | eu : {:.2f} 87 | ep : {:.2f} 88 | """.format( 89 | self.name, 90 | self.density, 91 | self.E, 92 | self.G, 93 | self.fy, 94 | self.fu, 95 | self.v, 96 | self.eu, 97 | self.ep, 98 | ) 99 | 100 | @property 101 | def __data__(self): 102 | data = super().__data__ 103 | data.update( 104 | { 105 | "fy": self.fy, 106 | "fu": self.fu, 107 | "eu": self.eu, 108 | "ep": self.ep, 109 | "tension": self.tension, 110 | "compression": self.compression, 111 | } 112 | ) 113 | return data 114 | 115 | @classmethod 116 | def __from_data__(cls, data): 117 | return cls( 118 | E=data["E"], 119 | v=data["v"], 120 | density=data["density"], 121 | fy=data["fy"], 122 | fu=data["fu"], 123 | eu=data["eu"], 124 | ) 125 | 126 | # TODO check values and make unit independent 127 | @classmethod 128 | def S355(cls, units=None): 129 | """Steel S355. 130 | 131 | Returns 132 | ------- 133 | :class:`compas_fea2.model.material.Steel` 134 | The precompiled steel material. 135 | """ 136 | if not units: 137 | units = u(system="SI_mm") 138 | elif not isinstance(units, UnitRegistry): 139 | units = u(system=units) 140 | 141 | return cls(fy=355 * units.MPa, fu=None, eu=20, E=210 * units.GPa, v=0.3, density=7850 * units("kg/m**3"), name=None) 142 | -------------------------------------------------------------------------------- /src/compas_fea2/model/materials/timber.py: -------------------------------------------------------------------------------- 1 | from .material import _Material 2 | 3 | 4 | class Timber(_Material): 5 | """Base class for Timber material""" 6 | 7 | def __init__(self, *, density, **kwargs): 8 | """ 9 | Parameters 10 | ---------- 11 | density : float 12 | Density of the timber material [kg/m^3]. 13 | name : str, optional 14 | Name of the material. 15 | """ 16 | super().__init__(density=density, **kwargs) 17 | 18 | @property 19 | def __data__(self): 20 | return { 21 | "density": self.density, 22 | "name": self.name, 23 | } 24 | 25 | @classmethod 26 | def __from_data__(cls, data): 27 | return cls( 28 | density=data["density"], 29 | name=data["name"], 30 | ) 31 | -------------------------------------------------------------------------------- /src/compas_fea2/model/releases.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING 2 | from typing import Union 3 | 4 | from compas_fea2.base import FEAData 5 | 6 | if TYPE_CHECKING: 7 | from .elements import BeamElement 8 | 9 | 10 | class _BeamEndRelease(FEAData): 11 | """Assign a general end release to a `compas_fea2.model.BeamElement`. 12 | 13 | Parameters 14 | ---------- 15 | n : bool, optional 16 | Release displacements along the local axial direction, by default False 17 | v1 : bool, optional 18 | Release displacements along local 1 direction, by default False 19 | v2 : bool, optional 20 | Release displacements along local 2 direction, by default False 21 | m1 : bool, optional 22 | Release rotations about local 1 direction, by default False 23 | m2 : bool, optional 24 | Release rotations about local 2 direction, by default False 25 | t : bool, optional 26 | Release rotations about local axial direction (torsion), by default False 27 | 28 | Attributes 29 | ---------- 30 | location : str 31 | 'start' or 'end' 32 | element : :class:`compas_fea2.model.BeamElement` 33 | The element to release. 34 | n : bool 35 | Release displacements along the local axial direction, by default False 36 | v1 : bool 37 | Release displacements along local 1 direction, by default False 38 | v2 : bool 39 | Release displacements along local 2 direction, by default False 40 | m1 : bool 41 | Release rotations about local 1 direction, by default False 42 | m2 : bool 43 | Release rotations about local 2 direction, by default False 44 | t : bool 45 | Release rotations about local axial direction (torsion), by default False 46 | 47 | """ 48 | 49 | def __init__( 50 | self, 51 | n: bool = False, 52 | v1: bool = False, 53 | v2: bool = False, 54 | m1: bool = False, 55 | m2: bool = False, 56 | t: bool = False, 57 | **kwargs, 58 | ): 59 | super().__init__(**kwargs) 60 | self._element: Union["BeamElement", None] = None 61 | self._location: Union[str, None] = None 62 | self.n: bool = n 63 | self.v1: bool = v1 64 | self.v2: bool = v2 65 | self.m1: bool = m1 66 | self.m2: bool = m2 67 | self.t: bool = t 68 | 69 | @property 70 | def element(self) -> Union["BeamElement", None]: 71 | return self._element 72 | 73 | @element.setter 74 | def element(self, value: "BeamElement"): 75 | if not isinstance(value, "BeamElement"): 76 | raise TypeError(f"{value!r} is not a beam element.") 77 | self._element = value 78 | 79 | @property 80 | def location(self) -> Union[str, None]: 81 | return self._location 82 | 83 | @location.setter 84 | def location(self, value: str): 85 | if value not in ("start", "end"): 86 | raise TypeError("the location can be either `start` or `end`") 87 | self._location = value 88 | 89 | @property 90 | def __data__(self): 91 | return { 92 | "class": self.__class__.__base__.__name__, 93 | "element": self._element, 94 | "location": self._location, 95 | "n": self.n, 96 | "v1": self.v1, 97 | "v2": self.v2, 98 | "m1": self.m1, 99 | "m2": self.m2, 100 | "t": self.t, 101 | } 102 | 103 | @classmethod 104 | def __from_data__(cls, data): 105 | obj = cls( 106 | n=data["n"], 107 | v1=data["v1"], 108 | v2=data["v2"], 109 | m1=data["m1"], 110 | m2=data["m2"], 111 | t=data["t"], 112 | ) 113 | obj._element = data["element"] 114 | obj._location = data["location"] 115 | return obj 116 | 117 | 118 | class BeamEndPinRelease(_BeamEndRelease): 119 | """Assign a pin end release to a `compas_fea2.model.BeamElement`. 120 | 121 | Parameters 122 | ---------- 123 | m1 : bool, optional 124 | Release rotations about local 1 direction, by default False 125 | m2 : bool, optional 126 | Release rotations about local 2 direction, by default False 127 | t : bool, optional 128 | Release rotations about local axial direction (torsion), by default False 129 | 130 | """ 131 | 132 | def __init__(self, m1: bool = False, m2: bool = False, t: bool = False, **kwargs): 133 | super().__init__(n=False, v1=False, v2=False, m1=m1, m2=m2, t=t, **kwargs) 134 | 135 | 136 | class BeamEndSliderRelease(_BeamEndRelease): 137 | """Assign a slider end release to a `compas_fea2.model.BeamElement`. 138 | 139 | Parameters 140 | ---------- 141 | v1 : bool, optional 142 | Release displacements along local 1 direction, by default False 143 | v2 : bool, optional 144 | Release displacements along local 2 direction, by default False 145 | 146 | """ 147 | 148 | def __init__(self, v1: bool = False, v2: bool = False, **kwargs): 149 | super().__init__(v1=v1, v2=v2, n=False, m1=False, m2=False, t=False, **kwargs) 150 | -------------------------------------------------------------------------------- /src/compas_fea2/problem/__init__.py: -------------------------------------------------------------------------------- 1 | from .problem import Problem 2 | from .displacements import GeneralDisplacement 3 | from .loads import ( 4 | Load, 5 | PrestressLoad, 6 | ConcentratedLoad, 7 | PressureLoad, 8 | GravityLoad, 9 | TributaryLoad, 10 | HarmonicPointLoad, 11 | HarmonicPressureLoad, 12 | ThermalLoad, 13 | ) 14 | 15 | from .fields import ( 16 | LoadField, 17 | DisplacementField, 18 | NodeLoadField, 19 | PointLoadField, 20 | _PrescribedField, 21 | PrescribedTemperatureField, 22 | ) 23 | from .combinations import LoadCombination 24 | 25 | from .steps import ( 26 | Step, 27 | GeneralStep, 28 | _Perturbation, 29 | ModalAnalysis, 30 | ComplexEigenValue, 31 | StaticStep, 32 | LinearStaticPerturbation, 33 | BucklingAnalysis, 34 | DynamicStep, 35 | QuasiStaticStep, 36 | DirectCyclicStep, 37 | ) 38 | 39 | 40 | __all__ = [ 41 | "Problem", 42 | "GeneralDisplacement", 43 | "Load", 44 | "PrestressLoad", 45 | "ConcentratedLoad", 46 | "PressureLoad", 47 | "GravityLoad", 48 | "TributaryLoad", 49 | "HarmonicPointLoad", 50 | "HarmonicPressureLoad", 51 | "ThermalLoad", 52 | "LoadField", 53 | "DisplacementField", 54 | "NodeLoadField", 55 | "PointLoadField", 56 | "LineLoadField", 57 | "PressureLoadField", 58 | "VolumeLoadField", 59 | "_PrescribedField", 60 | "PrescribedTemperatureField", 61 | "LoadCombination", 62 | "Step", 63 | "GeneralStep", 64 | "_Perturbation", 65 | "ModalAnalysis", 66 | "ComplexEigenValue", 67 | "StaticStep", 68 | "LinearStaticPerturbation", 69 | "BucklingAnalysis", 70 | "DynamicStep", 71 | "QuasiStaticStep", 72 | "DirectCyclicStep", 73 | "FieldOutput", 74 | "HistoryOutput", 75 | "DisplacementFieldOutput", 76 | "AccelerationFieldOutput", 77 | "VelocityFieldOutput", 78 | "Stress2DFieldOutput", 79 | "ReactionFieldOutput", 80 | "SectionForcesFieldOutput", 81 | ] 82 | -------------------------------------------------------------------------------- /src/compas_fea2/problem/combinations.py: -------------------------------------------------------------------------------- 1 | import compas_fea2 2 | from compas_fea2.base import FEAData 3 | 4 | 5 | class LoadCombination(FEAData): 6 | """Load combination used to combine load fields together at each step. 7 | 8 | Parameters 9 | ---------- 10 | factors : dict() 11 | Dictionary with the factors for each load case: {"load case": factor} 12 | """ 13 | 14 | def __init__(self, factors, **kwargs): 15 | super(LoadCombination, self).__init__(**kwargs) 16 | self.factors = factors 17 | 18 | @property 19 | def load_cases(self): 20 | for k in self.factors.keys(): 21 | yield k 22 | 23 | @property 24 | def step(self): 25 | return self._registration 26 | 27 | @property 28 | def problem(self): 29 | return self.step.problem 30 | 31 | @property 32 | def model(self): 33 | self.problem.model 34 | 35 | @classmethod 36 | def ULS(cls): 37 | return cls(factors={"DL": 1.35, "SDL": 1.35, "LL": 1.35}, name="ULS") 38 | 39 | @classmethod 40 | def SLS(cls): 41 | return cls(factors={"DL": 1, "SDL": 1, "LL": 1}, name="SLS") 42 | 43 | @classmethod 44 | def Fire(cls): 45 | return cls(factors={"DL": 1, "SDL": 1, "LL": 0.3}, name="Fire") 46 | 47 | @property 48 | def __data__(self): 49 | return { 50 | "factors": self.factors, 51 | "name": self.name, 52 | } 53 | 54 | @classmethod 55 | def __from_data__(cls, data): 56 | return cls(factors=data["factors"], name=data.get("name")) 57 | 58 | # BUG: Rewrite. this is not general and does not account for different loads types 59 | @property 60 | def node_load(self): 61 | """Generator returning each node and the corresponding total factored 62 | load of the combination. 63 | 64 | Returns 65 | ------- 66 | zip obj 67 | :class:`compas_fea2.model.node.Node`, :class:`compas_fea2.problem.loads.NodeLoad` 68 | """ 69 | nodes_loads = {} 70 | for load_field in self.step.load_fields: 71 | if isinstance(load_field, compas_fea2.problem.LoadField): 72 | if load_field.load_case in self.factors: 73 | for node in load_field.distribution: 74 | for load in load_field.loads: 75 | if node in nodes_loads: 76 | nodes_loads[node] += load * self.factors[load_field.load_case] 77 | else: 78 | nodes_loads[node] = load * self.factors[load_field.load_case] 79 | return zip(list(nodes_loads.keys()), list(nodes_loads.values())) 80 | -------------------------------------------------------------------------------- /src/compas_fea2/problem/displacements.py: -------------------------------------------------------------------------------- 1 | from compas_fea2.base import FEAData 2 | 3 | 4 | class GeneralDisplacement(FEAData): 5 | """GeneralDisplacement object. 6 | 7 | Parameters 8 | ---------- 9 | name : str, optional 10 | Uniqe identifier. If not provided it is automatically generated. Set a 11 | name if you want a more human-readable input file. 12 | x : float, optional 13 | x component of force, by default 0. 14 | y : float, optional 15 | y component of force, by default 0. 16 | z : float, optional 17 | z component of force, by default 0. 18 | xx : float, optional 19 | xx component of moment, by default 0. 20 | yy : float, optional 21 | yy component of moment, by default 0. 22 | zz : float, optional 23 | zz component of moment, by default 0. 24 | axes : str, optional 25 | BC applied via 'local' or 'global' axes, by default 'global'. 26 | 27 | Attributes 28 | ---------- 29 | name : str 30 | Uniqe identifier. If not provided it is automatically generated. Set a 31 | name if you want a more human-readable input file. 32 | x : float, optional 33 | x component of force, by default 0. 34 | y : float, optional 35 | y component of force, by default 0. 36 | z : float, optional 37 | z component of force, by default 0. 38 | xx : float, optional 39 | xx component of moment, by default 0. 40 | yy : float, optional 41 | yy component of moment, by default 0. 42 | zz : float, optional 43 | zz component of moment, by default 0. 44 | axes : str, optional 45 | BC applied via 'local' or 'global' axes, by default 'global'. 46 | 47 | Notes 48 | ----- 49 | Displacements are registered to a :class:`compas_fea2.problem.Step`. 50 | 51 | """ 52 | 53 | def __init__(self, x=0, y=0, z=0, xx=0, yy=0, zz=0, axes="global", **kwargs): 54 | super(GeneralDisplacement, self).__init__(**kwargs) 55 | self.x = x 56 | self.y = y 57 | self.z = z 58 | self.xx = xx 59 | self.yy = yy 60 | self.zz = zz 61 | self._axes = axes 62 | 63 | @property 64 | def axes(self): 65 | return self._axes 66 | 67 | @axes.setter 68 | def axes(self, value): 69 | self._axes = value 70 | 71 | @property 72 | def components(self): 73 | return {c: getattr(self, c) for c in ["x", "y", "z", "xx", "yy", "zz"]} 74 | 75 | @property 76 | def __data__(self): 77 | return { 78 | "x": self.x, 79 | "y": self.y, 80 | "z": self.z, 81 | "xx": self.xx, 82 | "yy": self.yy, 83 | "zz": self.zz, 84 | "axes": self._axes, 85 | } 86 | 87 | @classmethod 88 | def __from_data__(cls, data): 89 | return cls(x=data["x"], y=data["y"], z=data["z"], xx=data["xx"], yy=data["yy"], zz=data["zz"], axes=data["axes"]) 90 | -------------------------------------------------------------------------------- /src/compas_fea2/problem/fields.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable 2 | 3 | from compas_fea2.base import FEAData 4 | from compas_fea2.problem.loads import GravityLoad 5 | 6 | # TODO implement __*__ magic method for combination 7 | 8 | 9 | class LoadField(FEAData): 10 | """A pattern is the spatial distribution of a specific set of forces, 11 | displacements, temperatures, and other effects which act on a structure. 12 | Any combination of nodes and elements may be subjected to loading and 13 | kinematic conditions. 14 | 15 | Parameters 16 | ---------- 17 | load : :class:`compas_fea2.problem._Load` | :class:`compas_fea2.problem.GeneralDisplacement` 18 | The load/displacement assigned to the pattern. 19 | distribution : list 20 | List of :class:`compas_fea2.model.Node` or :class:`compas_fea2.model._Element`. The 21 | application in space of the load/displacement. 22 | load_case : str, optional 23 | The load case to which this pattern belongs. 24 | axes : str, optional 25 | Coordinate system for the load components. Default is "global". 26 | name : str, optional 27 | Unique identifier for the pattern. 28 | 29 | Attributes 30 | ---------- 31 | load : :class:`compas_fea2.problem._Load` 32 | The load of the pattern. 33 | distribution : list 34 | List of :class:`compas_fea2.model.Node` or :class:`compas_fea2.model._Element`. 35 | name : str 36 | Unique identifier. 37 | 38 | Notes 39 | ----- 40 | Patterns are registered to a :class:`compas_fea2.problem._Step`. 41 | """ 42 | 43 | def __init__( 44 | self, 45 | loads, 46 | distribution, 47 | load_case=None, 48 | **kwargs, 49 | ): 50 | super(LoadField, self).__init__(**kwargs) 51 | self._distribution = distribution if isinstance(distribution, Iterable) else [distribution] 52 | self._loads = loads if isinstance(loads, Iterable) else [loads * (1 / len(self._distribution))] * len(self._distribution) 53 | self.load_case = load_case 54 | self._registration = None 55 | 56 | @property 57 | def loads(self): 58 | return self._loads 59 | 60 | @property 61 | def distribution(self): 62 | return self._distribution 63 | 64 | @property 65 | def step(self): 66 | if self._registration: 67 | return self._registration 68 | else: 69 | raise ValueError("Register the Pattern to a Step first.") 70 | 71 | @property 72 | def problem(self): 73 | return self.step.problem 74 | 75 | @property 76 | def model(self): 77 | return self.problem.model 78 | 79 | # def __add__(self, other): 80 | # if not isinstance(other, Pattern): 81 | # raise TypeError("Can only combine with another Pattern") 82 | # combined_distribution = self._distribution + other._distribution 83 | # combined_components = {k: (getattr(self, k) or 0) + (getattr(other, k) or 0) for k in self.components} 84 | # return Pattern( 85 | # combined_distribution, 86 | # x=combined_components["x"], 87 | # y=combined_components["y"], 88 | # z=combined_components["z"], 89 | # xx=combined_components["xx"], 90 | # yy=combined_components["yy"], 91 | # zz=combined_components["zz"], 92 | # load_case=self.load_case or other.load_case, 93 | # axes=self.axes, 94 | # name=self.name or other.name, 95 | # ) 96 | 97 | 98 | class DisplacementField(LoadField): 99 | """A distribution of a set of displacements over a set of nodes. 100 | 101 | Parameters 102 | ---------- 103 | displacement : object 104 | The displacement to be applied. 105 | nodes : list 106 | List of nodes where the displacement is applied. 107 | load_case : object, optional 108 | The load case to which this pattern belongs. 109 | """ 110 | 111 | def __init__(self, displacements, nodes, load_case=None, **kwargs): 112 | nodes = nodes if isinstance(nodes, Iterable) else [nodes] 113 | displacements = displacements if isinstance(displacements, Iterable) else [displacements] * len(nodes) 114 | super(DisplacementField, self).__init__(loads=displacements, distribution=nodes, load_case=load_case, **kwargs) 115 | 116 | @property 117 | def nodes(self): 118 | return self._distribution 119 | 120 | @property 121 | def displacements(self): 122 | return self._loads 123 | 124 | @property 125 | def node_displacement(self): 126 | """Return a list of tuples with the nodes and the assigned displacement.""" 127 | return zip(self.nodes, self.displacements) 128 | 129 | 130 | class NodeLoadField(LoadField): 131 | """A distribution of a set of concentrated loads over a set of nodes. 132 | 133 | Parameters 134 | ---------- 135 | load : object 136 | The load to be applied. 137 | nodes : list 138 | List of nodes where the load is applied. 139 | load_case : object, optional 140 | The load case to which this pattern belongs. 141 | """ 142 | 143 | def __init__(self, loads, nodes, load_case=None, **kwargs): 144 | super(NodeLoadField, self).__init__(loads=loads, distribution=nodes, load_case=load_case, **kwargs) 145 | 146 | @property 147 | def nodes(self): 148 | return self._distribution 149 | 150 | @property 151 | def loads(self): 152 | return self._loads 153 | 154 | @property 155 | def node_load(self): 156 | """Return a list of tuples with the nodes and the assigned load.""" 157 | return zip(self.nodes, self.loads) 158 | 159 | 160 | class PointLoadField(NodeLoadField): 161 | """A distribution of a set of concentrated loads over a set of points. 162 | The loads are applied to the closest nodes to the points. 163 | 164 | Parameters 165 | ---------- 166 | load : object 167 | The load to be applied. 168 | points : list 169 | List of points where the load is applied. 170 | load_case : object, optional 171 | The load case to which this pattern belongs. 172 | tolerance : float, optional 173 | Tolerance for finding the closest nodes to the points. 174 | """ 175 | 176 | def __init__(self, loads, points, load_case=None, tolerance=1, **kwargs): 177 | self._points = points 178 | self._tolerance = tolerance 179 | # FIXME: this is not working, the patternhas no model! 180 | distribution = [self.model.find_closest_nodes_to_point(point, distance=self._tolerance)[0] for point in self.points] 181 | super().__init__(loads, distribution, load_case, **kwargs) 182 | 183 | @property 184 | def points(self): 185 | return self._points 186 | 187 | @property 188 | def nodes(self): 189 | return self._distribution 190 | 191 | 192 | class GravityLoadField(LoadField): 193 | """Volume distribution of a gravity load case. 194 | 195 | Parameters 196 | ---------- 197 | g : float 198 | Value of gravitational acceleration. 199 | parts : list 200 | List of parts where the load is applied. 201 | load_case : object, optional 202 | The load case to which this pattern belongs. 203 | """ 204 | 205 | def __init__(self, g=9.81, parts=None, load_case=None, **kwargs): 206 | super(GravityLoadField, self).__init__(GravityLoad(g=g), parts, load_case, **kwargs) 207 | 208 | 209 | class _PrescribedField(FEAData): 210 | """Base class for all predefined initial conditions. 211 | 212 | Notes 213 | ----- 214 | Fields are registered to a :class:`compas_fea2.problem.Step`. 215 | 216 | """ 217 | 218 | def __init__(self, **kwargs): 219 | super(_PrescribedField, self).__init__(**kwargs) 220 | 221 | 222 | class PrescribedTemperatureField(_PrescribedField): 223 | """Temperature field""" 224 | 225 | def __init__(self, temperature, **kwargs): 226 | super(PrescribedTemperatureField, self).__init__(**kwargs) 227 | self._t = temperature 228 | 229 | @property 230 | def temperature(self): 231 | return self._t 232 | 233 | @temperature.setter 234 | def temperature(self, value): 235 | self._t = value 236 | -------------------------------------------------------------------------------- /src/compas_fea2/problem/loads.py: -------------------------------------------------------------------------------- 1 | from compas_fea2.base import FEAData 2 | 3 | # TODO: make units independent using the utilities function 4 | 5 | 6 | class Load(FEAData): 7 | """Initialises base Load object. 8 | 9 | Parameters 10 | ---------- 11 | name : str 12 | Uniqe identifier. If not provided it is automatically generated. Set a 13 | name if you want a more human-readable input file. 14 | components : dict 15 | Load components. 16 | axes : str, optional 17 | Load applied via 'local' or 'global' axes, by default 'global'. 18 | 19 | Attributes 20 | ---------- 21 | name : str 22 | Uniqe identifier. If not provided it is automatically generated. Set a 23 | name if you want a more human-readable input file. 24 | components : dict 25 | Load components. These differ according to each Load type 26 | axes : str, optional 27 | Load applied via 'local' or 'global' axes, by default 'global'. 28 | 29 | Notes 30 | ----- 31 | Loads are registered to a :class:`compas_fea2.problem.Pattern`. 32 | 33 | """ 34 | 35 | def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes="global", **kwargs): 36 | super(Load, self).__init__(**kwargs) 37 | self.axes = axes 38 | self.x = x 39 | self.y = y 40 | self.z = z 41 | self.xx = xx 42 | self.yy = yy 43 | self.zz = zz 44 | 45 | @property 46 | def components(self): 47 | return {i: getattr(self, i) for i in ["x", "y", "z", "xx", "yy", "zz"]} 48 | 49 | @components.setter 50 | def components(self, value): 51 | for k, v in value: 52 | setattr(self, k, v) 53 | 54 | @property 55 | def pattern(self): 56 | return self._registration 57 | 58 | @property 59 | def step(self): 60 | return self.pattern._registration 61 | 62 | @property 63 | def problem(self): 64 | return self.step._registration 65 | 66 | @property 67 | def model(self): 68 | return self.problem._registration 69 | 70 | 71 | class ConcentratedLoad(Load): 72 | """Concentrated forces and moments [units:N, Nm]. 73 | 74 | Parameters 75 | ---------- 76 | x : float 77 | x component of force. 78 | y : float 79 | y component of force. 80 | z : float 81 | z component of force. 82 | xx : float 83 | xx component of moment. 84 | yy : float 85 | yy component of moment. 86 | zz : float 87 | zz component of moment. 88 | axes : str 89 | Load applied via 'local' or 'global' axes. 90 | 91 | Attributes 92 | ---------- 93 | name : str 94 | Automatically generated id. You can change the name if you want a more 95 | human readable input file. 96 | x : float 97 | x component of force. 98 | y : float 99 | y component of force. 100 | z : float 101 | z component of force. 102 | xx : float 103 | xx component of moment. 104 | yy : float 105 | yy component of moment. 106 | zz : float 107 | zz component of moment. 108 | axes : str 109 | Load applied via 'local' or 'global' axes. 110 | """ 111 | 112 | def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes="global", **kwargs): 113 | super(ConcentratedLoad, self).__init__(x=x, y=y, z=z, xx=xx, yy=yy, zz=zz, axes=axes, **kwargs) 114 | 115 | def __mul__(self, factor): 116 | if isinstance(factor, (float, int)): 117 | new_components = {k: (self.components[k] or 0) * factor for k in self.components} 118 | return ConcentratedLoad(**new_components, axes=self.axes) 119 | else: 120 | raise NotImplementedError 121 | 122 | def __rmul__(self, other): 123 | return self.__mul__(other) 124 | 125 | def __add__(self, other): 126 | if isinstance(other, ConcentratedLoad): 127 | new_components = {k: (self.components[k] or 0) + (other.components[k] or 0) for k in self.components} 128 | return ConcentratedLoad(**new_components, axes=self.axes) 129 | else: 130 | raise NotImplementedError 131 | 132 | def __radd__(self, other): 133 | return self.__add__(other) 134 | 135 | 136 | class PressureLoad(Load): 137 | """Distributed area force [e.g. units:N/m2] applied to element(s). 138 | 139 | Parameters 140 | ---------- 141 | elements : str, list 142 | Elements set or elements the load is applied to. 143 | x : float 144 | x component of force / area. 145 | y : float 146 | y component of force / area. 147 | z : float 148 | z component of force / area. 149 | 150 | Attributes 151 | ---------- 152 | name : str 153 | Automatically generated id. You can change the name if you want a more 154 | human readable input file. 155 | elements : str, list 156 | Elements set or elements the load is applied to. 157 | x : float 158 | x component of force / area. 159 | y : float 160 | y component of force / area. 161 | z : float 162 | z component of force / area. 163 | """ 164 | 165 | def __init__(self, x=0, y=0, z=0, axes="local", **kwargs): 166 | super(PressureLoad, self).__init__(components={"x": x, "y": y, "z": z}, axes=axes, **kwargs) 167 | raise NotImplementedError 168 | 169 | 170 | class GravityLoad(Load): 171 | """Gravity load [units:N/m3] applied to element(s). 172 | 173 | Parameters 174 | ---------- 175 | elements : str, list 176 | Element set or element keys the load is applied to. 177 | g : float 178 | Value of gravitational acceleration. 179 | x : float, optional 180 | Factor to apply to x direction, by default 0. 181 | y : float, optional 182 | Factor to apply to y direction, by default 0. 183 | z : float, optional 184 | Factor to apply to z direction, by default -1. 185 | 186 | Attributes 187 | ---------- 188 | name : str 189 | Automatically generated id. You can change the name if you want a more 190 | human readable input file. 191 | elements : str, list 192 | Element set or element keys the load is applied to. 193 | g : float 194 | Value of gravitational acceleration. 195 | x : float 196 | Factor to apply to x direction. 197 | y : float 198 | Factor to apply to y direction. 199 | z : float 200 | Factor to apply to z direction. 201 | 202 | Notes 203 | ----- 204 | By default gravity is supposed to act along the negative `z` axis. 205 | 206 | """ 207 | 208 | def __init__(self, g, x=0, y=0, z=-1, **kwargs): 209 | super(GravityLoad, self).__init__(x=x, y=y, z=z, axes="global", **kwargs) 210 | self._g = g 211 | 212 | @property 213 | def g(self): 214 | return self._g 215 | 216 | @property 217 | def vector(self): 218 | return [self.g * self.x, self.g * self.y, self.g * self.z] 219 | 220 | @property 221 | def components(self): 222 | components = {i: self.vector[j] for j, i in enumerate(["x", "y", "z"])} 223 | components.update({i: 0 for i in ["xx", "yy", "zz"]}) 224 | return components 225 | 226 | def __mul__(self, factor): 227 | if isinstance(factor, (float, int)): 228 | new_components = {k: (getattr(self, k) or 0) * factor for k in ["x", "y", "z"]} 229 | return GravityLoad(self.g, **new_components) 230 | else: 231 | raise NotImplementedError 232 | 233 | def __rmul__(self, other): 234 | return self.__mul__(other) 235 | 236 | 237 | class PrestressLoad(Load): 238 | """Prestress load""" 239 | 240 | def __init__(self, components, axes="global", **kwargs): 241 | super(TributaryLoad, self).__init__(components, axes, **kwargs) 242 | raise NotImplementedError 243 | 244 | 245 | class ThermalLoad(Load): 246 | """Thermal load""" 247 | 248 | def __init__(self, components, axes="global", **kwargs): 249 | super(ThermalLoad, self).__init__(components, axes, **kwargs) 250 | 251 | 252 | class TributaryLoad(Load): 253 | """Tributary load""" 254 | 255 | def __init__(self, components, axes="global", **kwargs): 256 | super(TributaryLoad, self).__init__(components, axes, **kwargs) 257 | raise NotImplementedError 258 | 259 | 260 | class HarmonicPointLoad(Load): 261 | """""" 262 | 263 | def __init__(self, components, axes="global", **kwargs): 264 | super(HarmonicPointLoad, self).__init__(components, axes, **kwargs) 265 | raise NotImplementedError 266 | 267 | 268 | class HarmonicPressureLoad(Load): 269 | """""" 270 | 271 | def __init__(self, components, axes="global", **kwargs): 272 | super(HarmonicPressureLoad, self).__init__(components, axes, **kwargs) 273 | raise NotImplementedError 274 | -------------------------------------------------------------------------------- /src/compas_fea2/problem/steps/__init__.py: -------------------------------------------------------------------------------- 1 | from .step import ( 2 | Step, 3 | GeneralStep, 4 | ) 5 | 6 | from .static import ( 7 | StaticStep, 8 | StaticRiksStep, 9 | ) 10 | 11 | from .dynamic import ( 12 | DynamicStep, 13 | ) 14 | 15 | from .quasistatic import ( 16 | QuasiStaticStep, 17 | DirectCyclicStep, 18 | ) 19 | 20 | from .perturbations import ( 21 | _Perturbation, 22 | ModalAnalysis, 23 | ComplexEigenValue, 24 | BucklingAnalysis, 25 | LinearStaticPerturbation, 26 | SteadyStateDynamic, 27 | SubstructureGeneration, 28 | ) 29 | 30 | __all__ = [ 31 | "Step", 32 | "GeneralStep", 33 | "_Perturbation", 34 | "ModalAnalysis", 35 | "ComplexEigenValue", 36 | "StaticStep", 37 | "StaticRiksStep", 38 | "LinearStaticPerturbation", 39 | "SteadyStateDynamic", 40 | "SubstructureGeneration", 41 | "BucklingAnalysis", 42 | "DynamicStep", 43 | "QuasiStaticStep", 44 | "DirectCyclicStep", 45 | ] 46 | -------------------------------------------------------------------------------- /src/compas_fea2/problem/steps/dynamic.py: -------------------------------------------------------------------------------- 1 | from .step import GeneralStep 2 | 3 | 4 | class DynamicStep(GeneralStep): 5 | """Step for dynamic analysis.""" 6 | 7 | def __init__(self, **kwargs): 8 | super(DynamicStep, self).__init__(**kwargs) 9 | raise NotImplementedError 10 | 11 | @property 12 | def __data__(self): 13 | data = super().__data__ 14 | # Add DynamicStep specific data here 15 | return data 16 | 17 | @classmethod 18 | def __from_data__(cls, data): 19 | obj = super(DynamicStep, cls).__from_data__(data) 20 | # Initialize DynamicStep specific attributes here 21 | return obj 22 | 23 | def add_harmonic_point_load(self): 24 | raise NotImplementedError 25 | 26 | def add_harmonic_preassure_load(self): 27 | raise NotImplementedError 28 | -------------------------------------------------------------------------------- /src/compas_fea2/problem/steps/perturbations.py: -------------------------------------------------------------------------------- 1 | from compas.geometry import Vector 2 | from compas.geometry import sum_vectors 3 | 4 | from compas_fea2.results import ModalAnalysisResult 5 | 6 | from .step import Step 7 | 8 | 9 | class _Perturbation(Step): 10 | """A perturbation is a change of the state of the structure after an analysis 11 | step. Perturbations' changes are not carried over to the next step. 12 | """ 13 | 14 | def __init__(self, **kwargs): 15 | super(_Perturbation, self).__init__(**kwargs) 16 | 17 | @property 18 | def __data__(self) -> dict: 19 | data = super().__data__ 20 | data.update( 21 | { 22 | "type": self.__class__.__name__, 23 | } 24 | ) 25 | return data 26 | 27 | @classmethod 28 | def __from_data__(cls, data): 29 | return cls(**data) 30 | 31 | 32 | class ModalAnalysis(_Perturbation): 33 | """Perform a modal analysis of the Model from the resulting state after an 34 | analysis Step. 35 | 36 | Parameters 37 | ---------- 38 | modes : int 39 | Number of modes. 40 | 41 | """ 42 | 43 | def __init__(self, modes=1, **kwargs): 44 | super(ModalAnalysis, self).__init__(**kwargs) 45 | self.modes = modes 46 | 47 | @property 48 | def rdb(self): 49 | return self.problem.rdb 50 | 51 | def _get_results_from_db(self, mode, **kwargs): 52 | """Get the results for the given members and steps. 53 | 54 | Parameters 55 | ---------- 56 | members : _type_ 57 | _description_ 58 | steps : _type_ 59 | _description_ 60 | 61 | Returns 62 | ------- 63 | _type_ 64 | _description_ 65 | """ 66 | filters = {} 67 | filters["step"] = [self.name] 68 | filters["mode"] = [mode] 69 | 70 | # Get the eigenvalue 71 | eigenvalue = self.rdb.get_rows("eigenvalues", ["lambda"], filters)[0][0] 72 | 73 | # Get the eiginvectors 74 | all_columns = ["step", "part", "key", "x", "y", "z", "xx", "yy", "zz"] 75 | results_set = self.rdb.get_rows("eigenvectors", ["step", "part", "key", "x", "y", "z", "xx", "yy", "zz"], filters) 76 | results_set = [{k: v for k, v in zip(all_columns, row)} for row in results_set] 77 | eigenvector = self.rdb.to_result(results_set, "find_node_by_key", "u")[self] 78 | 79 | return eigenvalue, eigenvector 80 | 81 | @property 82 | def results(self): 83 | for mode in range(self.modes): 84 | yield self.mode_result(mode + 1) 85 | 86 | @property 87 | def frequencies(self): 88 | for mode in range(self.modes): 89 | yield self.mode_frequency(mode + 1) 90 | 91 | @property 92 | def shapes(self): 93 | for mode in range(self.modes): 94 | yield self.mode_shape(mode + 1) 95 | 96 | def mode_shape(self, mode): 97 | return self.mode_result(mode).shape 98 | 99 | def mode_frequency(self, mode): 100 | return self.mode_result(mode).frequency 101 | 102 | def mode_result(self, mode): 103 | eigenvalue, eigenvector = self._get_results_from_db(mode) 104 | return ModalAnalysisResult(step=self, mode=mode, eigenvalue=eigenvalue, eigenvector=eigenvector) 105 | 106 | def show_mode_shape(self, mode, fast=True, opacity=1, scale_results=1, show_bcs=True, show_original=0.25, show_contour=False, show_vectors=False, **kwargs): 107 | """Show the mode shape of a given mode. 108 | 109 | Parameters 110 | ---------- 111 | mode : int 112 | The mode to show. 113 | fast : bool, optional 114 | Show the mode shape fast, by default True 115 | opacity : float, optional 116 | Opacity of the model, by default 1 117 | scale_results : float, optional 118 | Scale the results, by default 1 119 | show_bcs : bool, optional 120 | Show the boundary conditions, by default True 121 | show_original : float, optional 122 | Show the original model, by default 0.25 123 | show_contour : bool, optional 124 | Show the contour, by default False 125 | show_vectors : bool, optional 126 | Show the vectors, by default False 127 | 128 | """ 129 | from compas_fea2.UI import FEA2Viewer 130 | 131 | viewer = FEA2Viewer(center=self.model.center, scale_model=1) 132 | 133 | if show_original: 134 | viewer.add_model(self.model, show_parts=True, fast=True, opacity=show_original, show_bcs=False, **kwargs) 135 | 136 | shape = self.mode_shape(mode) 137 | if show_vectors: 138 | viewer.add_mode_shape(shape, fast=fast, show_parts=False, component=None, show_vectors=show_vectors, show_contour=show_contour, **kwargs) 139 | 140 | # TODO create a copy of the model first 141 | for displacement in shape.results: 142 | vector = displacement.vector.scaled(scale_results) 143 | displacement.node.xyz = sum_vectors([Vector(*displacement.location.xyz), vector]) 144 | 145 | if show_contour: 146 | viewer.add_mode_shape(shape, fast=fast, component=None, show_vectors=False, show_contour=show_contour, **kwargs) 147 | viewer.add_model(self.model, fast=fast, opacity=opacity, show_bcs=show_bcs, **kwargs) 148 | viewer.show() 149 | 150 | @property 151 | def __data__(self): 152 | data = super().__data__ 153 | data.update( 154 | { 155 | "modes": self.modes, 156 | } 157 | ) 158 | return data 159 | 160 | @classmethod 161 | def __from_data__(cls, data): 162 | return cls(modes=data["modes"], **data) 163 | 164 | 165 | class ComplexEigenValue(_Perturbation): 166 | """""" 167 | 168 | def __init__(self, **kwargs): 169 | super().__init__(**kwargs) 170 | raise NotImplementedError 171 | 172 | @property 173 | def __data__(self): 174 | return super().__data__ 175 | 176 | @classmethod 177 | def __from_data__(cls, data): 178 | return cls(**data) 179 | 180 | 181 | class BucklingAnalysis(_Perturbation): 182 | """""" 183 | 184 | def __init__(self, modes, vectors=None, iterations=30, algorithm=None, **kwargs): 185 | super().__init__(**kwargs) 186 | self._modes = modes 187 | self._vectors = vectors or self._compute_vectors(modes) 188 | self._iterations = iterations 189 | self._algorithm = algorithm 190 | 191 | def _compute_vectors(self, modes): 192 | self._vectors = modes * 2 193 | if modes > 9: 194 | self._vectors += modes 195 | 196 | @staticmethod 197 | def Lanczos(modes): 198 | return BucklingAnalysis(modes=modes, vectors=None, algorithhm="Lanczos") 199 | 200 | @staticmethod 201 | def Subspace( 202 | modes, 203 | iterations, 204 | vectors=None, 205 | ): 206 | return BucklingAnalysis( 207 | modes=modes, 208 | vectors=vectors, 209 | iterations=iterations, 210 | algorithhm="Subspace", 211 | ) 212 | 213 | @property 214 | def __data__(self): 215 | data = super().__data__ 216 | data.update( 217 | { 218 | "modes": self._modes, 219 | "vectors": self._vectors, 220 | "iterations": self._iterations, 221 | "algorithm": self._algorithm, 222 | } 223 | ) 224 | return data 225 | 226 | @classmethod 227 | def __from_data__(cls, data): 228 | return cls(modes=data["_modes"], vectors=data["_vectors"], iterations=data["_iterations"], algorithm=data["_algorithm"], **data) 229 | 230 | 231 | class LinearStaticPerturbation(_Perturbation): 232 | """""" 233 | 234 | def __init__(self, **kwargs): 235 | super().__init__(**kwargs) 236 | raise NotImplementedError 237 | 238 | @property 239 | def __data__(self): 240 | return super().__data__ 241 | 242 | @classmethod 243 | def __from_data__(cls, data): 244 | return cls(**data) 245 | 246 | 247 | class SteadyStateDynamic(_Perturbation): 248 | """""" 249 | 250 | def __init__(self, **kwargs): 251 | super().__init__(**kwargs) 252 | raise NotImplementedError 253 | 254 | @property 255 | def __data__(self): 256 | return super().__data__ 257 | 258 | @classmethod 259 | def __from_data__(cls, data): 260 | return cls(**data) 261 | 262 | 263 | class SubstructureGeneration(_Perturbation): 264 | """""" 265 | 266 | def __init__(self, **kwargs): 267 | super().__init__(**kwargs) 268 | raise NotImplementedError 269 | 270 | @property 271 | def __data__(self): 272 | return super().__data__ 273 | 274 | @classmethod 275 | def __from_data__(cls, data): 276 | return cls(**data) 277 | -------------------------------------------------------------------------------- /src/compas_fea2/problem/steps/quasistatic.py: -------------------------------------------------------------------------------- 1 | from .step import GeneralStep 2 | 3 | 4 | class QuasiStaticStep(GeneralStep): 5 | """Step for quasi-static analysis.""" 6 | 7 | def __init__(self, **kwargs): 8 | super(QuasiStaticStep, self).__init__(**kwargs) 9 | raise NotImplementedError 10 | 11 | @property 12 | def __data__(self): 13 | data = super().__data__ 14 | # Add specific data for QuasiStaticStep 15 | return data 16 | 17 | @classmethod 18 | def __from_data__(cls, data): 19 | obj = super(QuasiStaticStep, cls).__from_data__(data) 20 | # Initialize specific attributes for QuasiStaticStep 21 | return obj 22 | 23 | 24 | class DirectCyclicStep(GeneralStep): 25 | """Step for a direct cyclic analysis.""" 26 | 27 | def __init__(self, **kwargs): 28 | super(DirectCyclicStep, self).__init__(**kwargs) 29 | raise NotImplementedError 30 | 31 | @property 32 | def __data__(self): 33 | data = super().__data__ 34 | # Add specific data for DirectCyclicStep 35 | return data 36 | 37 | @classmethod 38 | def __from_data__(cls, data): 39 | obj = super(DirectCyclicStep, cls).__from_data__(data) 40 | # Initialize specific attributes for DirectCyclicStep 41 | return obj 42 | -------------------------------------------------------------------------------- /src/compas_fea2/problem/steps/static.py: -------------------------------------------------------------------------------- 1 | from .step import GeneralStep 2 | 3 | 4 | class StaticStep(GeneralStep): 5 | """StaticStep for use in a static analysis. 6 | 7 | Parameters 8 | ---------- 9 | max_increments : int 10 | Max number of increments to perform during the case step. 11 | (Typically 100 but you might have to increase it in highly non-linear 12 | problems. This might increase the analysis time.). 13 | initial_inc_size : float 14 | Sets the the size of the increment for the first iteration. 15 | (By default is equal to the total time, meaning that the software decrease 16 | the size automatically.) 17 | min_inc_size : float 18 | Minimum increment size before stopping the analysis. 19 | (By default is 1e-5, but you can set a smaller size for highly non-linear 20 | problems. This might increase the analysis time.) 21 | time : float 22 | Total time of the case step. Note that this not actual 'time', 23 | but rather a proportionality factor. (By default is 1, meaning that the 24 | analysis is complete when all the increments sum up to 1) 25 | nlgeom : bool 26 | if ``True`` nonlinear geometry effects are considered. 27 | modify : bool 28 | if ``True`` the loads applied in a previous step are substituted by the 29 | ones defined in the present step, otherwise the loads are added. 30 | 31 | Attributes 32 | ---------- 33 | name : str 34 | Automatically generated id. You can change the name if you want a more 35 | human readable input file. 36 | max_increments : int 37 | Max number of increments to perform during the case step. 38 | (Typically 100 but you might have to increase it in highly non-linear 39 | problems. This might increase the analysis time.). 40 | initial_inc_size : float 41 | Sets the the size of the increment for the first iteration. 42 | (By default is equal to the total time, meaning that the software decrease 43 | the size automatically.) 44 | min_inc_size : float 45 | Minimum increment size before stopping the analysis. 46 | (By default is 1e-5, but you can set a smaller size for highly non-linear 47 | problems. This might increase the analysis time.) 48 | time : float 49 | Total time of the case step. Note that this not actual 'time', 50 | but rather a proportionality factor. (By default is 1, meaning that the 51 | analysis is complete when all the increments sum up to 1) 52 | nlgeom : bool 53 | if ``True`` nonlinear geometry effects are considered. 54 | modify : bool 55 | if ``True`` the loads applied in a previous step are substituted by the 56 | ones defined in the present step, otherwise the loads are added. 57 | loads : dict 58 | Dictionary of the loads assigned to each part in the model in the step. 59 | displacements : dict 60 | Dictionary of the displacements assigned to each part in the model in the step. 61 | 62 | """ 63 | 64 | def __init__( 65 | self, 66 | max_increments=100, 67 | initial_inc_size=1, 68 | min_inc_size=0.00001, 69 | max_inc_size=1, 70 | time=1, 71 | nlgeom=False, 72 | modify=True, 73 | **kwargs, 74 | ): 75 | super().__init__( 76 | max_increments=max_increments, 77 | initial_inc_size=initial_inc_size, 78 | min_inc_size=min_inc_size, 79 | max_inc_size=max_inc_size, 80 | time=time, 81 | nlgeom=nlgeom, 82 | modify=modify, 83 | **kwargs, 84 | ) 85 | 86 | @property 87 | def __data__(self): 88 | return { 89 | "max_increments": self.max_increments, 90 | "initial_inc_size": self.initial_inc_size, 91 | "min_inc_size": self.min_inc_size, 92 | "time": self.time, 93 | "nlgeom": self.nlgeom, 94 | "modify": self.modify, 95 | # Add other attributes as needed 96 | } 97 | 98 | @classmethod 99 | def __from_data__(cls, data): 100 | return cls( 101 | max_increments=data["max_increments"], 102 | initial_inc_size=data["initial_inc_size"], 103 | min_inc_size=data["min_inc_size"], 104 | time=data["time"], 105 | nlgeom=data["nlgeom"], 106 | modify=data["modify"], 107 | # Add other attributes as needed 108 | ) 109 | 110 | 111 | class StaticRiksStep(StaticStep): 112 | """Step for use in a static analysis when Riks method is necessary.""" 113 | 114 | def __init__( 115 | self, 116 | max_increments=100, 117 | initial_inc_size=1, 118 | min_inc_size=0.00001, 119 | time=1, 120 | nlgeom=False, 121 | modify=True, 122 | **kwargs, 123 | ): 124 | super().__init__(max_increments, initial_inc_size, min_inc_size, time, nlgeom, modify, **kwargs) 125 | raise NotImplementedError 126 | -------------------------------------------------------------------------------- /src/compas_fea2/problem/steps_combinations.py: -------------------------------------------------------------------------------- 1 | from compas_fea2.base import FEAData 2 | 3 | 4 | class StepsCombination(FEAData): 5 | """A StepsCombination `sums` the analysis results of given steps 6 | (:class:`compas_fea2.problem.LoadPattern`). 7 | 8 | Parameters 9 | ---------- 10 | FEAData : _type_ 11 | _description_ 12 | 13 | Notes 14 | ----- 15 | By default every analysis in `compas_fea2` is meant to be `non-linear`, in 16 | the sense that the effects of a load pattern (:class:`compas_fea2.problem.Pattern`) 17 | in a given steps are used as a starting point for the application of the load 18 | patterns in the next step. Therefore, the sequence of the steps can affect 19 | the results (if the response is actully non-linear). 20 | 21 | """ 22 | 23 | def __init__(self, **kwargs): 24 | raise NotImplementedError() 25 | -------------------------------------------------------------------------------- /src/compas_fea2/results/__init__.py: -------------------------------------------------------------------------------- 1 | from .results import ( 2 | Result, 3 | DisplacementResult, 4 | AccelerationResult, 5 | VelocityResult, 6 | ReactionResult, 7 | StressResult, 8 | MembraneStressResult, 9 | ShellStressResult, 10 | SolidStressResult, 11 | ) 12 | 13 | from .fields import ( 14 | DisplacementFieldResults, 15 | AccelerationFieldResults, 16 | VelocityFieldResults, 17 | StressFieldResults, 18 | ReactionFieldResults, 19 | SectionForcesFieldResults, 20 | ContactForcesFieldResults, 21 | ) 22 | 23 | from .modal import ( 24 | ModalAnalysisResult, 25 | ModalShape, 26 | ) 27 | 28 | 29 | __all__ = [ 30 | "Result", 31 | "DisplacementResult", 32 | "AccelerationResult", 33 | "VelocityResult", 34 | "ReactionResult", 35 | "StressResult", 36 | "MembraneStressResult", 37 | "ShellStressResult", 38 | "SolidStressResult", 39 | "DisplacementFieldResults", 40 | "AccelerationFieldResults", 41 | "VelocityFieldResults", 42 | "ReactionFieldResults", 43 | "StressFieldResults", 44 | "ContactForcesFieldResults", 45 | "SectionForcesFieldResults", 46 | "ModalAnalysisResult", 47 | "ModalShape", 48 | ] 49 | -------------------------------------------------------------------------------- /src/compas_fea2/results/histories.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | 3 | 4 | class StressHistoryResult: 5 | def __init__(self, **kwargs): 6 | super(StressHistoryResult, self).__init__(**kwargs) 7 | self.stress_history = [] # Initialize an empty list to store stress history 8 | 9 | def add_result(self, result): 10 | """ 11 | Updates the stress tensor and stores the stress state in the history. 12 | """ 13 | self.stress_history.append(result) 14 | 15 | def plot_stress_path(self, stress_components=("S11", "S22")): 16 | """ 17 | Plots the stress path for the specified stress components. 18 | :param stress_components: A tuple of the stress components to plot (default is ('S11', 'S22')). 19 | """ 20 | # Extract the stress components from the history 21 | stress_x = [stress[stress_components[0]] for stress in self.stress_history] 22 | stress_y = [stress[stress_components[1]] for stress in self.stress_history] 23 | 24 | # Create the plot 25 | plt.figure(figsize=(8, 6)) 26 | plt.plot(stress_x, stress_y, "-o", label="Stress Path") 27 | plt.xlabel(f"{stress_components[0]} (Pa)") 28 | plt.ylabel(f"{stress_components[1]} (Pa)") 29 | plt.title("Stress Path") 30 | plt.grid(True) 31 | plt.legend() 32 | plt.show() 33 | -------------------------------------------------------------------------------- /src/compas_fea2/results/modal.py: -------------------------------------------------------------------------------- 1 | # from .results import Result 2 | import numpy as np 3 | 4 | from compas_fea2.base import FEAData 5 | 6 | from .fields import NodeFieldResults 7 | 8 | 9 | class ModalAnalysisResult(FEAData): 10 | """Modal analysis result. 11 | 12 | Parameters 13 | ---------- 14 | mode : int 15 | Mode number. 16 | eigenvalue : float 17 | Eigenvalue. 18 | eigenvector : list 19 | List of DisplacementResult objects. 20 | 21 | Attributes 22 | ---------- 23 | mode : int 24 | Mode number. 25 | eigenvalue : float 26 | Eigenvalue. 27 | frequency : float 28 | Frequency of the mode. 29 | omega : float 30 | Angular frequency of the mode. 31 | period : float 32 | Period of the mode. 33 | eigenvector : list 34 | List of DisplacementResult objects. 35 | """ 36 | 37 | _field_name = "eigen" 38 | _results_func = "find_node_by_key" 39 | _components_names = ["x", "y", "z", "xx", "yy", "zz"] 40 | _invariants_names = ["magnitude"] 41 | 42 | def __init__(self, *, step, mode, eigenvalue, eigenvector, **kwargs): 43 | super(ModalAnalysisResult, self).__init__(**kwargs) 44 | self.step = step 45 | self._mode = mode 46 | self._eigenvalue = eigenvalue 47 | self._eigenvector = eigenvector 48 | 49 | @property 50 | def mode(self): 51 | return self._mode 52 | 53 | @property 54 | def eigenvalue(self): 55 | return self._eigenvalue 56 | 57 | @property 58 | def frequency(self): 59 | return self.omega / (2 * np.pi) 60 | 61 | @property 62 | def omega(self): 63 | return np.sqrt(self._eigenvalue) 64 | 65 | @property 66 | def period(self): 67 | return 1 / self.frequency 68 | 69 | @property 70 | def eigenvector(self): 71 | return self._eigenvector 72 | 73 | @property 74 | def shape(self): 75 | return ModalShape(step=self.step, results=self._eigenvector) 76 | 77 | def _normalize_eigenvector(self): 78 | """ 79 | Normalize the eigenvector to obtain the mode shape. 80 | Mode shapes are typically scaled so the maximum displacement is 1. 81 | """ 82 | max_val = np.max(np.abs(self._eigenvector)) 83 | return self._eigenvector / max_val if max_val != 0 else self._eigenvector 84 | 85 | def participation_factor(self, mass_matrix): 86 | """ 87 | Calculate the modal participation factor. 88 | :param mass_matrix: Global mass matrix. 89 | :return: Participation factor. 90 | """ 91 | if len(self.eigenvector) != len(mass_matrix): 92 | raise ValueError("Eigenvector length must match the mass matrix size") 93 | return np.dot(self.eigenvector.T, np.dot(mass_matrix, self.eigenvector)) 94 | 95 | def modal_contribution(self, force_vector): 96 | """ 97 | Calculate the contribution of this mode to the global response for a given force vector. 98 | :param force_vector: External force vector. 99 | :return: Modal contribution. 100 | """ 101 | return np.dot(self.eigenvector, force_vector) / self.eigenvalue 102 | 103 | def to_dict(self): 104 | """ 105 | Export the modal analysis result as a dictionary. 106 | """ 107 | return { 108 | "mode": self.mode, 109 | "eigenvalue": self.eigenvalue, 110 | "frequency": self.frequency, 111 | "omega": self.omega, 112 | "period": self.period, 113 | "eigenvector": self.eigenvector.tolist(), 114 | "mode_shape": self.mode_shape.tolist(), 115 | } 116 | 117 | def to_json(self, filepath): 118 | import json 119 | 120 | with open(filepath, "w") as f: 121 | json.dump(self.to_dict(), f, indent=4) 122 | 123 | def to_csv(self, filepath): 124 | import csv 125 | 126 | with open(filepath, "w", newline="") as f: 127 | writer = csv.writer(f) 128 | writer.writerow(["Mode", "Eigenvalue", "Frequency", "Omega", "Period", "Eigenvector", "Mode Shape"]) 129 | writer.writerow([self.mode, self.eigenvalue, self.frequency, self.omega, self.period, ", ".join(map(str, self.eigenvector)), ", ".join(map(str, self.mode_shape))]) 130 | 131 | def __repr__(self): 132 | return f"ModalAnalysisResult(mode={self.mode}, eigenvalue={self.eigenvalue:.4f}, frequency={self.frequency:.4f} Hz, period={self.period:.4f} s)" 133 | 134 | 135 | class ModalShape(NodeFieldResults): 136 | """ModalShape result applied as Displacement field. 137 | 138 | Parameters 139 | ---------- 140 | step : :class:`compas_fea2.problem.Step` 141 | The analysis step 142 | results : list 143 | List of DisplcementResult objects. 144 | """ 145 | 146 | def __init__(self, step, results, *args, **kwargs): 147 | super(ModalShape, self).__init__(step=step, results_cls=ModalAnalysisResult, *args, **kwargs) 148 | self._results = results 149 | self._field_name = "eigen" 150 | 151 | @property 152 | def results(self): 153 | return self._results 154 | 155 | def _get_results_from_db(self, members=None, columns=None, filters=None, **kwargs): 156 | raise NotImplementedError("this method is not applicable for ModalShape results") 157 | 158 | def get_result_at(self, location): 159 | raise NotImplementedError("this method is not applicable for ModalShape results") 160 | 161 | def get_max_result(self, component): 162 | raise NotImplementedError("this method is not applicable for ModalShape results") 163 | 164 | def get_min_result(self, component): 165 | raise NotImplementedError("this method is not applicable for ModalShape results") 166 | 167 | def get_max_component(self, component): 168 | raise NotImplementedError("this method is not applicable for ModalShape results") 169 | 170 | def get_min_component(self, component): 171 | raise NotImplementedError("this method is not applicable for ModalShape results") 172 | 173 | def get_limits_component(self, component): 174 | raise NotImplementedError("this method is not applicable for ModalShape results") 175 | 176 | def get_limits_absolute(self): 177 | raise NotImplementedError("this method is not applicable for ModalShape results") 178 | -------------------------------------------------------------------------------- /src/compas_fea2/units/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pint import UnitRegistry 3 | 4 | HERE = os.path.dirname(__file__) 5 | 6 | # U.define('@alias pascal = Pa') 7 | 8 | 9 | def units(system="SI"): 10 | return UnitRegistry(os.path.join(HERE, "fea2_en.txt"), system=system) 11 | -------------------------------------------------------------------------------- /src/compas_fea2/units/constants_en.txt: -------------------------------------------------------------------------------- 1 | # Default Pint constants definition file 2 | # Based on the International System of Units 3 | # Language: english 4 | # Source: https://physics.nist.gov/cuu/Constants/ 5 | # https://physics.nist.gov/PhysRefData/XrayTrans/Html/search.html 6 | # :copyright: 2013,2019 by Pint Authors, see AUTHORS for more details. 7 | 8 | #### MATHEMATICAL CONSTANTS #### 9 | # As computed by Maxima with fpprec:50 10 | 11 | pi = 3.1415926535897932384626433832795028841971693993751 = π # pi 12 | tansec = 4.8481368111333441675396429478852851658848753880815e-6 # tangent of 1 arc-second ~ arc_second/radian 13 | ln10 = 2.3025850929940456840179914546843642076011014886288 # natural logarithm of 10 14 | wien_x = 4.9651142317442763036987591313228939440555849867973 # solution to (x-5)*exp(x)+5 = 0 => x = W(5/exp(5))+5 15 | wien_u = 2.8214393721220788934031913302944851953458817440731 # solution to (u-3)*exp(u)+3 = 0 => u = W(3/exp(3))+3 16 | eulers_number = 2.71828182845904523536028747135266249775724709369995 17 | 18 | #### DEFINED EXACT CONSTANTS #### 19 | 20 | speed_of_light = 299792458 m/s = c = c_0 # since 1983 21 | planck_constant = 6.62607015e-34 J s = ℎ # since May 2019 22 | elementary_charge = 1.602176634e-19 C = e # since May 2019 23 | avogadro_number = 6.02214076e23 # since May 2019 24 | boltzmann_constant = 1.380649e-23 J K^-1 = k = k_B # since May 2019 25 | standard_gravity = 9.80665 m/s^2 = g_0 = g0 = g_n = gravity # since 1901 26 | standard_atmosphere = 1.01325e5 Pa = atm = atmosphere # since 1954 27 | conventional_josephson_constant = 4.835979e14 Hz / V = K_J90 # since Jan 1990 28 | conventional_von_klitzing_constant = 2.5812807e4 ohm = R_K90 # since Jan 1990 29 | 30 | #### DERIVED EXACT CONSTANTS #### 31 | # Floating-point conversion may introduce inaccuracies 32 | 33 | zeta = c / (cm/s) = ζ 34 | dirac_constant = ℎ / (2 * π) = ħ = hbar = atomic_unit_of_action = a_u_action 35 | avogadro_constant = avogadro_number * mol^-1 = N_A 36 | molar_gas_constant = k * N_A = R 37 | faraday_constant = e * N_A 38 | conductance_quantum = 2 * e ** 2 / ℎ = G_0 39 | magnetic_flux_quantum = ℎ / (2 * e) = Φ_0 = Phi_0 40 | josephson_constant = 2 * e / ℎ = K_J 41 | von_klitzing_constant = ℎ / e ** 2 = R_K 42 | stefan_boltzmann_constant = 2 / 15 * π ** 5 * k ** 4 / (ℎ ** 3 * c ** 2) = σ = sigma 43 | first_radiation_constant = 2 * π * ℎ * c ** 2 = c_1 44 | second_radiation_constant = ℎ * c / k = c_2 45 | wien_wavelength_displacement_law_constant = ℎ * c / (k * wien_x) 46 | wien_frequency_displacement_law_constant = wien_u * k / ℎ 47 | 48 | #### MEASURED CONSTANTS #### 49 | # Recommended CODATA-2018 values 50 | # To some extent, what is measured and what is derived is a bit arbitrary. 51 | # The choice of measured constants is based on convenience and on available uncertainty. 52 | # The uncertainty in the last significant digits is given in parentheses as a comment. 53 | 54 | newtonian_constant_of_gravitation = 6.67430e-11 m^3/(kg s^2) = _ = gravitational_constant # (15) 55 | rydberg_constant = 1.0973731568160e7 * m^-1 = R_∞ = R_inf # (21) 56 | electron_g_factor = -2.00231930436256 = g_e # (35) 57 | atomic_mass_constant = 1.66053906660e-27 kg = m_u # (50) 58 | electron_mass = 9.1093837015e-31 kg = m_e = atomic_unit_of_mass = a_u_mass # (28) 59 | proton_mass = 1.67262192369e-27 kg = m_p # (51) 60 | neutron_mass = 1.67492749804e-27 kg = m_n # (95) 61 | lattice_spacing_of_Si = 1.920155716e-10 m = d_220 # (32) 62 | K_alpha_Cu_d_220 = 0.80232719 # (22) 63 | K_alpha_Mo_d_220 = 0.36940604 # (19) 64 | K_alpha_W_d_220 = 0.108852175 # (98) 65 | 66 | #### DERIVED CONSTANTS #### 67 | 68 | fine_structure_constant = (2 * ℎ * R_inf / (m_e * c)) ** 0.5 = α = alpha 69 | vacuum_permeability = 2 * α * ℎ / (e ** 2 * c) = µ_0 = mu_0 = mu0 = magnetic_constant 70 | vacuum_permittivity = e ** 2 / (2 * α * ℎ * c) = ε_0 = epsilon_0 = eps_0 = eps0 = electric_constant 71 | impedance_of_free_space = 2 * α * ℎ / e ** 2 = Z_0 = characteristic_impedance_of_vacuum 72 | coulomb_constant = α * hbar * c / e ** 2 = k_C 73 | classical_electron_radius = α * hbar / (m_e * c) = r_e 74 | thomson_cross_section = 8 / 3 * π * r_e ** 2 = σ_e = sigma_e 75 | -------------------------------------------------------------------------------- /src/compas_fea2/utilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/src/compas_fea2/utilities/__init__.py -------------------------------------------------------------------------------- /src/compas_fea2/utilities/_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import division 3 | from __future__ import print_function 4 | 5 | import itertools 6 | import os 7 | import subprocess 8 | import sys 9 | import threading 10 | import time 11 | from functools import wraps 12 | from time import perf_counter 13 | from typing import Generator 14 | from typing import Optional 15 | 16 | from compas_fea2 import VERBOSE 17 | 18 | 19 | def with_spinner(message="Running"): 20 | """Decorator to add a spinner animation to a function.""" 21 | 22 | def decorator(func): 23 | @wraps(func) 24 | def wrapper(*args, **kwargs): 25 | stop_event = threading.Event() 26 | spinner_thread = threading.Thread(target=spinner_animation, args=(message, stop_event)) 27 | spinner_thread.start() 28 | try: 29 | result = func(*args, **kwargs) 30 | return result 31 | finally: 32 | stop_event.set() 33 | spinner_thread.join() 34 | 35 | return wrapper 36 | 37 | return decorator 38 | 39 | 40 | def spinner_animation(message, stop_event): 41 | """Spinner animation for indicating progress.""" 42 | spinner = "|/-\\" 43 | idx = 0 44 | while not stop_event.is_set(): 45 | sys.stdout.write(f"\r{message} {spinner[idx % len(spinner)]}") 46 | sys.stdout.flush() 47 | time.sleep(0.2) # Adjust for speed 48 | idx += 1 49 | sys.stdout.write("\rDone! \n") # Clear the line when done 50 | 51 | 52 | def timer(_func=None, *, message=None): 53 | """Print the runtime of the decorated function""" 54 | 55 | def decorator_timer(func): 56 | @wraps(func) 57 | def wrapper_timer(*args, **kwargs): 58 | start_time = perf_counter() # 1 59 | value = func(*args, **kwargs) 60 | end_time = perf_counter() # 2 61 | run_time = end_time - start_time # 3 62 | if VERBOSE: 63 | m = message or "Finished {!r} in".format(func.__name__) 64 | print("{} {:.4f} secs".format(m, run_time)) 65 | return value 66 | 67 | return wrapper_timer 68 | 69 | if _func is None: 70 | return decorator_timer 71 | else: 72 | return decorator_timer(_func) 73 | 74 | 75 | def launch_process(cmd_args: list[str], cwd: Optional[str] = None, verbose: bool = False, **kwargs) -> Generator[bytes, None, None]: 76 | """Open a subprocess and yield its output line by line. 77 | 78 | Parameters 79 | ---------- 80 | cmd_args : list[str] 81 | List of command arguments to execute. 82 | cwd : str, optional 83 | Path where to start the subprocess, by default None. 84 | verbose : bool, optional 85 | Print the output of the subprocess, by default `False`. 86 | 87 | Yields 88 | ------ 89 | bytes 90 | Output lines from the subprocess. 91 | 92 | Raises 93 | ------ 94 | FileNotFoundError 95 | If the command executable is not found. 96 | subprocess.CalledProcessError 97 | If the subprocess exits with a non-zero return code. 98 | """ 99 | try: 100 | env = os.environ.copy() 101 | with subprocess.Popen(cmd_args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd, shell=True, env=env, **kwargs) as process: 102 | assert process.stdout is not None 103 | for line in process.stdout: 104 | yield line.decode(errors="replace").strip() 105 | 106 | process.wait() 107 | if process.returncode != 0: 108 | raise subprocess.CalledProcessError(process.returncode, cmd_args) 109 | 110 | except FileNotFoundError as e: 111 | print(f"Error: Command not found - {e}") 112 | raise 113 | except subprocess.CalledProcessError as e: 114 | print(f"Error: Command '{cmd_args}' failed with return code {e.returncode}") 115 | raise 116 | 117 | 118 | class extend_docstring: 119 | def __init__(self, method, note=False): 120 | self.doc = method.__doc__ 121 | 122 | def __call__(self, function): 123 | if self.doc is not None: 124 | doc = function.__doc__ 125 | function.__doc__ = self.doc 126 | if doc is not None: 127 | function.__doc__ += doc 128 | return function 129 | 130 | 131 | def get_docstring(cls): 132 | """ 133 | Decorator: Append to a function's docstring. 134 | """ 135 | 136 | def _decorator(func): 137 | func_name = func.__qualname__.split(".")[-1] 138 | doc_parts = getattr(cls, func_name).original.__doc__.split("Returns") 139 | note = """ 140 | Returns 141 | ------- 142 | list of {} 143 | 144 | """.format( 145 | doc_parts[1].split("-------\n")[1] 146 | ) 147 | func.__doc__ = doc_parts[0] + note 148 | return func 149 | 150 | return _decorator 151 | 152 | 153 | def part_method(f): 154 | """Run a part level method. In this way it is possible to bring to the 155 | model level some of the functions of the parts. 156 | 157 | Parameters 158 | ---------- 159 | method : str 160 | name of the method to call. 161 | 162 | Returns 163 | ------- 164 | [var] 165 | List results of the method per each part in the model. 166 | """ 167 | 168 | @wraps(f) 169 | def wrapper(*args, **kwargs): 170 | func_name = f.__qualname__.split(".")[-1] 171 | self_obj = args[0] 172 | res = [vars for part in self_obj.parts if (vars := getattr(part, func_name)(*args[1::], **kwargs))] 173 | if not res: 174 | return res 175 | # if res is a list of lists 176 | elif isinstance(res[0], list): 177 | res = list(itertools.chain.from_iterable(res)) 178 | return res 179 | # if res is a Group 180 | elif "Group" in str(res[0].__class__): 181 | combined_members = set.union(*(group._members for group in res)) 182 | return res[0].__class__(combined_members) 183 | else: 184 | return res 185 | 186 | return wrapper 187 | 188 | 189 | def step_method(f): 190 | """Run a step level method. In this way it is possible to bring to the 191 | problem level some of the functions of the steps. 192 | 193 | Parameters 194 | ---------- 195 | method : str 196 | name of the method to call. 197 | 198 | Returns 199 | ------- 200 | [var] 201 | List results of the method per each step in the problem. 202 | """ 203 | 204 | @wraps(f) 205 | def wrapper(*args, **kwargs): 206 | func_name = f.__qualname__.split(".")[-1] 207 | self_obj = args[0] 208 | res = [vars for step in self_obj.steps if (vars := getattr(step, func_name)(*args[1:], **kwargs))] 209 | res = list(itertools.chain.from_iterable(res)) 210 | return res 211 | 212 | return wrapper 213 | 214 | 215 | def problem_method(f): 216 | """Run a problem level method. In this way it is possible to bring to the 217 | model level some of the functions of the problems. 218 | 219 | Parameters 220 | ---------- 221 | method : str 222 | name of the method to call. 223 | 224 | Returns 225 | ------- 226 | [var] 227 | List results of the method per each problem in the model. 228 | """ 229 | 230 | @wraps(f) 231 | def wrapper(*args, **kwargs): 232 | func_name = f.__qualname__.split(".")[-1] 233 | self_obj = args[0] 234 | res = [vars for problem in self_obj.problems if (vars := getattr(problem, func_name)(*args[1::], **kwargs))] 235 | res = list(itertools.chain.from_iterable(res)) 236 | return res 237 | 238 | return wrapper 239 | 240 | 241 | def to_dimensionless(func): 242 | """Decorator to convert pint Quantity objects to dimensionless in the base units.""" 243 | 244 | def wrapper(*args, **kwargs): 245 | new_args = [a.to_base_units().magnitude if hasattr(a, "to_base_units") else a for a in args] 246 | new_kwargs = {k: v.to_base_units().magnitude if hasattr(v, "to_base_units") else v for k, v in kwargs.items()} 247 | return func(*new_args, **new_kwargs) 248 | 249 | wrapper.original = func # Preserve the original function 250 | return wrapper 251 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import os 4 | 5 | from compas_invocations2 import build 6 | from compas_invocations2 import docs 7 | from compas_invocations2 import style 8 | from compas_invocations2 import tests 9 | from invoke import Collection 10 | 11 | ns = Collection( 12 | docs.help, 13 | style.check, 14 | style.lint, 15 | style.format, 16 | docs.docs, 17 | docs.linkcheck, 18 | tests.test, 19 | tests.testdocs, 20 | build.build_ghuser_components, 21 | build.prepare_changelog, 22 | build.clean, 23 | build.release, 24 | ) 25 | ns.configure( 26 | { 27 | "base_folder": os.path.dirname(__file__), 28 | "ghuser": { 29 | "source_dir": "src/compas_notebook/ghpython/components", 30 | "target_dir": "src/compas_notebook/ghpython/components/ghuser", 31 | }, 32 | } 33 | ) 34 | -------------------------------------------------------------------------------- /temp/PLACEHOLDER: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/compas-dev/compas_fea2/1010ccc85030d645389031ea4ab4d314d313ac64/temp/PLACEHOLDER -------------------------------------------------------------------------------- /tests/test_bcs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from compas_fea2.model.bcs import FixedBC, PinnedBC, RollerBCX 3 | 4 | 5 | class TestBCs(unittest.TestCase): 6 | def test_fixed_bc(self): 7 | bc = FixedBC() 8 | self.assertTrue(bc.x) 9 | self.assertTrue(bc.y) 10 | self.assertTrue(bc.z) 11 | self.assertTrue(bc.xx) 12 | self.assertTrue(bc.yy) 13 | self.assertTrue(bc.zz) 14 | 15 | def test_pinned_bc(self): 16 | bc = PinnedBC() 17 | self.assertTrue(bc.x) 18 | self.assertTrue(bc.y) 19 | self.assertTrue(bc.z) 20 | self.assertFalse(bc.xx) 21 | self.assertFalse(bc.yy) 22 | self.assertFalse(bc.zz) 23 | 24 | def test_roller_bc_x(self): 25 | bc = RollerBCX() 26 | self.assertFalse(bc.x) 27 | self.assertTrue(bc.y) 28 | self.assertTrue(bc.z) 29 | self.assertFalse(bc.xx) 30 | self.assertFalse(bc.yy) 31 | self.assertFalse(bc.zz) 32 | 33 | 34 | if __name__ == "__main__": 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /tests/test_connectors.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from compas_fea2.model.connectors import SpringConnector, ZeroLengthSpringConnector 3 | from compas_fea2.model import Node 4 | from compas_fea2.model import Part 5 | from compas_fea2.model import SpringSection 6 | 7 | 8 | class TestSpringConnector(unittest.TestCase): 9 | def test_initialization(self): 10 | node1 = Node([0, 0, 0]) 11 | prt_1 = Part() 12 | prt_1.add_node(node1) 13 | node2 = Node([1, 0, 0]) 14 | prt_2 = Part() 15 | prt_2.add_node(node2) 16 | section = SpringSection(axial=1, lateral=1, rotational=1) # Replace with actual section class 17 | connector = SpringConnector(nodes=[node1, node2], section=section) 18 | self.assertEqual(connector.nodes, [node1, node2]) 19 | 20 | 21 | class TestZeroLengthSpringConnector(unittest.TestCase): 22 | def test_initialization(self): 23 | node1 = Node([0, 0, 0]) 24 | prt_1 = Part() 25 | prt_1.add_node(node1) 26 | node2 = Node([1, 0, 0]) 27 | prt_2 = Part() 28 | prt_2.add_node(node2) 29 | direction = [1, 0, 0] 30 | section = SpringSection(axial=1, lateral=1, rotational=1) 31 | connector = ZeroLengthSpringConnector(nodes=[node1, node2], direction=direction, section=section) 32 | self.assertEqual(connector.nodes, [node1, node2]) 33 | self.assertEqual(connector.direction, direction) 34 | 35 | 36 | if __name__ == "__main__": 37 | unittest.main() 38 | -------------------------------------------------------------------------------- /tests/test_constraints.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestTieMPC(unittest.TestCase): 5 | def test_initialization(self): 6 | pass 7 | 8 | 9 | class TestBeamMPC(unittest.TestCase): 10 | def test_initialization(self): 11 | pass 12 | 13 | 14 | if __name__ == "__main__": 15 | unittest.main() 16 | -------------------------------------------------------------------------------- /tests/test_elements.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from compas_fea2.model.elements import BeamElement, ShellElement, TetrahedronElement 3 | from compas_fea2.model import Node, Steel, RectangularSection 4 | 5 | 6 | class TestBeamElement(unittest.TestCase): 7 | def test_initialization(self): 8 | node1 = Node([0, 0, 0]) 9 | node2 = Node([1, 0, 0]) 10 | mat = Steel.S355() 11 | section = RectangularSection(w=1, h=2, material=mat) 12 | element = BeamElement(nodes=[node1, node2], section=section, frame=[0, 0, 1]) 13 | self.assertEqual(element.nodes, [node1, node2]) 14 | 15 | 16 | class TestShellElement(unittest.TestCase): 17 | def test_initialization(self): 18 | node1 = Node([0, 0, 0]) 19 | node2 = Node([1, 0, 0]) 20 | node3 = Node([1, 1, 0]) 21 | element = ShellElement(nodes=[node1, node2, node3], section=None) 22 | self.assertEqual(element.nodes, [node1, node2, node3]) 23 | 24 | 25 | class TestTetrahedronElement(unittest.TestCase): 26 | def test_initialization(self): 27 | node1 = Node([0, 0, 0]) 28 | node2 = Node([1, 0, 0]) 29 | node3 = Node([1, 1, 0]) 30 | node4 = Node([0, 1, 1]) 31 | element = TetrahedronElement(nodes=[node1, node2, node3, node4], section=None) 32 | self.assertEqual(element.nodes, [node1, node2, node3, node4]) 33 | 34 | 35 | if __name__ == "__main__": 36 | unittest.main() 37 | -------------------------------------------------------------------------------- /tests/test_groups.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from compas_fea2.model.groups import NodesGroup, ElementsGroup, FacesGroup, PartsGroup 3 | from compas_fea2.model import Node, BeamElement, Part, ShellElement, ShellSection, Steel 4 | 5 | 6 | class TestNodesGroup(unittest.TestCase): 7 | def test_add_node(self): 8 | node = Node([0, 0, 0]) 9 | group = NodesGroup(nodes=[node]) 10 | self.assertIn(node, group.nodes) 11 | 12 | 13 | class TestElementsGroup(unittest.TestCase): 14 | def test_add_element(self): 15 | node1 = Node([0, 0, 0]) 16 | node2 = Node([1, 0, 0]) 17 | mat = Steel.S355() 18 | section = ShellSection(0.1, material=mat) 19 | element = BeamElement(nodes=[node1, node2], section=section, frame=[0, 0, 1]) 20 | group = ElementsGroup(elements=[element]) 21 | self.assertIn(element, group.elements) 22 | 23 | 24 | class TestFacesGroup(unittest.TestCase): 25 | def test_add_face(self): 26 | node1 = Node([0, 0, 0]) 27 | node2 = Node([1, 0, 0]) 28 | node3 = Node([1, 1, 0]) 29 | nodes = [node1, node2, node3] 30 | mat = Steel.S355() 31 | section = ShellSection(0.1, material=mat) 32 | element = ShellElement(nodes=nodes, section=section) 33 | face = element.faces[0] 34 | group = FacesGroup(faces=element.faces) 35 | self.assertIn(face, group.faces) 36 | 37 | 38 | class TestPartsGroup(unittest.TestCase): 39 | def test_add_part(self): 40 | part = Part() 41 | group = PartsGroup(parts=[part]) 42 | self.assertIn(part, group.parts) 43 | 44 | 45 | if __name__ == "__main__": 46 | unittest.main() 47 | -------------------------------------------------------------------------------- /tests/test_ics.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from compas_fea2.model.ics import InitialTemperatureField, InitialStressField 3 | 4 | 5 | class TestInitialTemperatureField(unittest.TestCase): 6 | def test_initialization(self): 7 | ic = InitialTemperatureField(temperature=100) 8 | self.assertEqual(ic.temperature, 100) 9 | 10 | def test_temperature_setter(self): 11 | ic = InitialTemperatureField(temperature=100) 12 | ic.temperature = 200 13 | self.assertEqual(ic.temperature, 200) 14 | 15 | 16 | class TestInitialStressField(unittest.TestCase): 17 | def test_initialization(self): 18 | ic = InitialStressField(stress=(10, 20, 30)) 19 | self.assertEqual(ic.stress, (10, 20, 30)) 20 | 21 | def test_stress_setter(self): 22 | ic = InitialStressField(stress=(10, 20, 30)) 23 | ic.stress = (40, 50, 60) 24 | self.assertEqual(ic.stress, (40, 50, 60)) 25 | 26 | 27 | if __name__ == "__main__": 28 | unittest.main() 29 | -------------------------------------------------------------------------------- /tests/test_model.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from compas_fea2.model.model import Model 3 | from compas_fea2.model.parts import Part 4 | from compas_fea2.problem import Problem 5 | 6 | 7 | class TestModel(unittest.TestCase): 8 | def test_add_part(self): 9 | model = Model() 10 | part = Part() 11 | model.add_part(part) 12 | self.assertIn(part, model.parts) 13 | 14 | def test_find_part_by_name(self): 15 | model = Model() 16 | part = Part(name="test_part") 17 | model.add_part(part) 18 | found_part = model.find_part_by_name("test_part") 19 | self.assertEqual(found_part, part) 20 | 21 | def test_add_problem(self): 22 | model = Model() 23 | problem = Problem() # Replace with actual problem class 24 | model.add_problem(problem) 25 | self.assertIn(problem, model.problems) 26 | 27 | 28 | if __name__ == "__main__": 29 | unittest.main() 30 | -------------------------------------------------------------------------------- /tests/test_nodes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from compas_fea2.model.nodes import Node 3 | from compas.geometry import Point 4 | 5 | 6 | class TestNode(unittest.TestCase): 7 | def test_initialization(self): 8 | node = Node([1, 2, 3]) 9 | self.assertEqual(node.xyz, [1, 2, 3]) 10 | self.assertEqual(node.mass, [None, None, None, None, None, None]) 11 | self.assertIsNone(node.temperature) 12 | 13 | def test_mass_setter(self): 14 | node = Node([1, 2, 3], mass=[10, 10, 10, 10, 10, 10]) 15 | self.assertEqual(node.mass, [10, 10, 10, 10, 10, 10]) 16 | node.mass = [5, 5, 5, 5, 5, 5] 17 | self.assertEqual(node.mass, [5, 5, 5, 5, 5, 5]) 18 | 19 | def test_temperature_setter(self): 20 | node = Node([1, 2, 3], temperature=100) 21 | self.assertEqual(node.temperature, 100) 22 | node.temperature = 200 23 | self.assertEqual(node.temperature, 200) 24 | 25 | def test_gkey(self): 26 | node = Node([1, 2, 3]) 27 | self.assertIsNotNone(node.gkey) 28 | 29 | def test_from_compas_point(self): 30 | point = Point(1, 2, 3) 31 | node = Node.from_compas_point(point) 32 | self.assertEqual(node.xyz, [1, 2, 3]) 33 | 34 | 35 | if __name__ == "__main__": 36 | unittest.main() 37 | -------------------------------------------------------------------------------- /tests/test_parts.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from compas_fea2.model.parts import Part, RigidPart 3 | from compas_fea2.model import Node, BeamElement 4 | from compas_fea2.model import Steel 5 | from compas_fea2.model import RectangularSection 6 | 7 | 8 | class TestPart(unittest.TestCase): 9 | def test_add_node(self): 10 | part = Part() 11 | node = Node([0, 0, 0]) 12 | part.add_node(node) 13 | self.assertIn(node, part.nodes) 14 | 15 | def test_add_element(self): 16 | part = Part() 17 | node1 = Node([0, 0, 0]) 18 | node2 = Node([1, 0, 0]) 19 | part.add_node(node1) 20 | part.add_node(node2) 21 | section = RectangularSection(w=1, h=1, material=Steel.S355()) 22 | element = BeamElement(nodes=[node1, node2], section=section, frame=[0, 0, 1]) 23 | part.add_element(element) 24 | self.assertIn(element, part.elements) 25 | 26 | def test_add_material(self): 27 | part = Part() 28 | material = Steel.S355() 29 | part.add_material(material) 30 | self.assertIn(material, part.materials) 31 | 32 | def test_add_section(self): 33 | part = Part() 34 | material = Steel.S355() 35 | section = RectangularSection(w=1, h=1, material=material) 36 | part.add_section(section) 37 | self.assertIn(section, part.sections) 38 | 39 | 40 | class TestRigidPart(unittest.TestCase): 41 | def test_reference_point(self): 42 | part = RigidPart() 43 | node = Node([0, 0, 0]) 44 | part.reference_point = node 45 | self.assertEqual(part.reference_point, node) 46 | 47 | 48 | if __name__ == "__main__": 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /tests/test_placeholder.py: -------------------------------------------------------------------------------- 1 | def test_placeholder(): 2 | assert True 3 | -------------------------------------------------------------------------------- /tests/test_releases.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from compas_fea2.model.releases import _BeamEndRelease, BeamEndPinRelease, BeamEndSliderRelease 3 | 4 | 5 | class TestBeamEndRelease(unittest.TestCase): 6 | def test_initialization(self): 7 | release = _BeamEndRelease(n=True, v1=True, v2=True, m1=True, m2=True, t=True) 8 | self.assertTrue(release.n) 9 | self.assertTrue(release.v1) 10 | self.assertTrue(release.v2) 11 | self.assertTrue(release.m1) 12 | self.assertTrue(release.m2) 13 | self.assertTrue(release.t) 14 | 15 | def test_element_setter(self): 16 | pass 17 | 18 | def test_location_setter(self): 19 | release = _BeamEndRelease() 20 | with self.assertRaises(TypeError): 21 | release.location = "middle" 22 | release.location = "start" 23 | self.assertEqual(release.location, "start") 24 | 25 | 26 | class TestBeamEndPinRelease(unittest.TestCase): 27 | def test_initialization(self): 28 | release = BeamEndPinRelease(m1=True, m2=True, t=True) 29 | self.assertTrue(release.m1) 30 | self.assertTrue(release.m2) 31 | self.assertTrue(release.t) 32 | self.assertFalse(release.n) 33 | self.assertFalse(release.v1) 34 | self.assertFalse(release.v2) 35 | 36 | 37 | class TestBeamEndSliderRelease(unittest.TestCase): 38 | def test_initialization(self): 39 | release = BeamEndSliderRelease(v1=True, v2=True) 40 | self.assertTrue(release.v1) 41 | self.assertTrue(release.v2) 42 | self.assertFalse(release.n) 43 | self.assertFalse(release.m1) 44 | self.assertFalse(release.m2) 45 | self.assertFalse(release.t) 46 | 47 | 48 | if __name__ == "__main__": 49 | unittest.main() 50 | -------------------------------------------------------------------------------- /tests/test_sections.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from compas_fea2.model.sections import RectangularSection, CircularSection, ISection 3 | from compas_fea2.model.materials.steel import Steel 4 | 5 | 6 | class TestSections(unittest.TestCase): 7 | def setUp(self): 8 | self.material = Steel.S355() 9 | 10 | def test_rectangular_section(self): 11 | section = RectangularSection(w=100, h=50, material=self.material) 12 | self.assertEqual(section.shape.w, 100) 13 | self.assertEqual(section.shape.h, 50) 14 | self.assertAlmostEqual(section.A, 5000) 15 | self.assertEqual(section.material, self.material) 16 | 17 | def test_circular_section(self): 18 | section = CircularSection(r=10, material=self.material) 19 | self.assertEqual(section.shape.radius, 10) 20 | self.assertAlmostEqual(section.A, 314.14, places=2) 21 | self.assertEqual(section.material, self.material) 22 | 23 | def test_isection(self): 24 | section = ISection(w=100, h=200, tw=10, ttf=20, tbf=20, material=self.material) 25 | self.assertEqual(section.shape.w, 100) 26 | self.assertEqual(section.shape.h, 200) 27 | self.assertEqual(section.shape.tw, 10) 28 | self.assertEqual(section.shape.tbf, 20) 29 | self.assertEqual(section.shape.ttf, 20) 30 | self.assertEqual(section.material, self.material) 31 | 32 | 33 | if __name__ == "__main__": 34 | unittest.main() 35 | -------------------------------------------------------------------------------- /tests/test_shapes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from compas.geometry import Point, Frame 3 | from compas_fea2.model.shapes import Rectangle, Circle, IShape, Shape 4 | 5 | 6 | class TestShapes(unittest.TestCase): 7 | def test_rectangle(self): 8 | rect = Rectangle(w=100, h=50) 9 | self.assertEqual(rect.w, 100) 10 | self.assertEqual(rect.h, 50) 11 | self.assertAlmostEqual(rect.A, 5000) 12 | self.assertIsInstance(rect.centroid, Point) 13 | self.assertEqual(rect.centroid.x, 0) 14 | self.assertEqual(rect.centroid.y, 0) 15 | self.assertEqual(rect.centroid.z, 0) 16 | self.assertAlmostEqual(rect.Ixx, 100 * 50**3 / 12, 3) 17 | self.assertAlmostEqual(rect.Iyy, 100**3 * 50 / 12, 3) 18 | self.assertAlmostEqual(rect.J, 2_861_002.60, places=2) 19 | self.assertAlmostEqual(rect.Avx, 4_166.67, places=2) 20 | self.assertAlmostEqual(rect.Avy, 4_166.67, places=2) 21 | 22 | def test_circle(self): 23 | circle = Circle(radius=10) 24 | self.assertEqual(circle.radius, 10) 25 | self.assertAlmostEqual(circle.A, 314.159, places=0) 26 | self.assertIsInstance(circle.centroid, Point) 27 | self.assertAlmostEqual(circle.Ixx, 7853, 0) 28 | self.assertAlmostEqual(circle.Iyy, 7853, 0) 29 | self.assertAlmostEqual(circle.J, 15708, places=0) 30 | self.assertAlmostEqual(circle.Avx, 283, places=0) 31 | self.assertAlmostEqual(circle.Avy, 283, places=0) 32 | 33 | def test_ishape(self): 34 | ishape = IShape(w=100, h=200, tw=10, tbf=20, ttf=20) 35 | self.assertEqual(ishape.w, 100) 36 | self.assertEqual(ishape.h, 200) 37 | self.assertEqual(ishape.tw, 10) 38 | self.assertEqual(ishape.tbf, 20) 39 | self.assertEqual(ishape.ttf, 20) 40 | self.assertIsInstance(ishape.centroid, Point) 41 | 42 | def test_shape_translation(self): 43 | rect = Rectangle(w=100, h=50) 44 | translated_rect = rect.translated([10, 20, 30]) 45 | self.assertIsInstance(translated_rect, Shape) 46 | self.assertNotEqual(rect.centroid, translated_rect.centroid) 47 | 48 | def test_shape_orientation(self): 49 | rect = Rectangle(w=100, h=50) 50 | new_frame = Frame([0, 0, 1000], [1, 0, 0], [0, 1, 0]) 51 | oriented_rect = rect.oriented(new_frame) 52 | self.assertIsInstance(oriented_rect, Shape) 53 | self.assertNotEqual(rect.centroid, oriented_rect.centroid) 54 | 55 | 56 | if __name__ == "__main__": 57 | unittest.main() 58 | --------------------------------------------------------------------------------