diff --git a/.editorconfig b/.editorconfig index e7c99f5d9..c300dcab8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,4 @@ # EditorConfig is awesome: http://EditorConfig.org - root = true [*] @@ -9,7 +8,7 @@ insert_final_newline = true indent_style = space indent_size = 4 charset = utf-8 -max_line_length = 120 +max_line_length = 180 [*.{bat,cmd,ps1}] end_of_line = crlf @@ -17,13 +16,12 @@ end_of_line = crlf [*.md] trim_trailing_whitespace = false +[*.yml] +indent_size = 2 + [Makefile] indent_style = tab indent_size = 4 -[*.yml] -indent_style = space -indent_size = 2 - [LICENSE] insert_final_newline = false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc4ee6bc6..3c8ecf99f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,9 +18,9 @@ jobs: python: ['3.8', '3.9', '3.10'] steps: - - uses: compas-dev/compas-actions.build@v2 + - uses: compas-dev/compas-actions.build@v3 with: - test_lint: true - test_compas: true + invoke_lint: true + check_import: true use_conda: true python: ${{ matrix.python }} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 5a683392b..b577432c3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,7 +14,8 @@ jobs: docs: runs-on: windows-latest steps: - - uses: compas-dev/compas-actions.docs@v2 + - uses: compas-dev/compas-actions.docs@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} use_conda: true + doc_url: https://compas.dev/compas_fea2/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b775bf307..24adf3076 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,12 +14,12 @@ jobs: python: ['3.8', '3.9', '3.10'] steps: - - uses: compas-dev/compas-actions.build@v2 + - uses: compas-dev/compas-actions.build@v3 with: - test_lint: true - test_compas: true - use_conda: true python: ${{ matrix.python }} + invoke_lint: true + use_conda: true + check_import: true Publish: needs: build diff --git a/CHANGELOG.md b/CHANGELOG.md index f85771c2e..d893f59d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Updated existing workflows to latest. * Build tests are temporarily disabled. +* Updated `compas-actions.build` and `compas-actions.doc` workflows to v3. ### Removed -* Support for python below 3.8 \ No newline at end of file +* Support for python below 3.8 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cbe992ce5..136be039d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,61 +1,3 @@ # Contributing -Contributions are welcome and very much appreciated! - - -## Code contributions - -We accept code contributions through pull requests. -In short, this is how that works. - -1. Fork [the repository](https://github.com/compas-dev/compas_fea2) and clone the fork. -2. Create a virtual environment using your tool of choice (e.g. `virtualenv`, `conda`, etc). -3. Install development dependencies: - -```bash - $ pip install -r requirements-dev.txt -``` - -4. Make sure all tests pass: - -```bash - $ invoke test -``` - -5. Start making your changes to the **master** branch (or branch off of it). -6. Make sure all tests still pass: - -```bash - $ invoke test -``` - -7. Add yourself to the *Contributors* section of `AUTHORS.md`. -8. Commit your changes and push your branch to GitHub. -9. Create a [pull request](https://help.github.com/articles/about-pull-requests/) through the GitHub website. - - -During development, use [pyinvoke](http://docs.pyinvoke.org/) tasks on the -command line to ease recurring operations: - -* `invoke clean`: Clean all generated artifacts. -* `invoke check`: Run various code and documentation style checks. -* `invoke docs`: Generate documentation. -* `invoke test`: Run all tests and checks in one swift command. -* `invoke`: Show available tasks. - - -## Bug reports - -When [reporting a bug](https://github.com/compas-dev/compas_fea2/issues) please include: - -* Operating system name and version. -* Any details about your local setup that might be helpful in troubleshooting. -* Detailed steps to reproduce the bug. - - -## Feature requests - -When [proposing a new feature](https://github.com/compas-dev/compas_fea2/issues) please include: - -* Explain in detail how it would work. -* Keep the scope as narrow as possible, to make it easier to implement. +Coming soon... diff --git a/MANIFEST.in b/MANIFEST.in index a448717e8..2429a09b2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -18,5 +18,7 @@ exclude pytest.ini .bumpversion.cfg .editorconfig exclude tasks.py exclude CONTRIBUTING.md exclude conftest.py +exclude environment.yml +exclude *.cff global-exclude *.py[cod] __pycache__ *.dylib *.nb[ic] .DS_Store diff --git a/docs/_images/CollaborationWorkflow.jpg b/docs/_images/CollaborationWorkflow.jpg new file mode 100644 index 000000000..3df9de11a Binary files /dev/null and b/docs/_images/CollaborationWorkflow.jpg differ diff --git a/docs/_images/CollaborationWorkflow_example.jpg b/docs/_images/CollaborationWorkflow_example.jpg new file mode 100644 index 000000000..ef82f5ba8 Binary files /dev/null and b/docs/_images/CollaborationWorkflow_example.jpg differ diff --git a/docs/_images/examples/principal_stress_cantilever.png b/docs/_images/examples/principal_stress_cantilever.png deleted file mode 100644 index 2178c1240..000000000 Binary files a/docs/_images/examples/principal_stress_cantilever.png and /dev/null differ diff --git a/docs/_images/examples/principal_stress_script.png b/docs/_images/examples/principal_stress_script.png deleted file mode 100644 index 9f47855c2..000000000 Binary files a/docs/_images/examples/principal_stress_script.png and /dev/null differ diff --git a/docs/_images/fork.png b/docs/_images/fork.png new file mode 100644 index 000000000..32c3a360f Binary files /dev/null and b/docs/_images/fork.png differ diff --git a/docs/_images/workflow_1.png b/docs/_images/workflow_1.png new file mode 100644 index 000000000..115898b70 Binary files /dev/null and b/docs/_images/workflow_1.png differ diff --git a/docs/_images/workflow_2.jpg b/docs/_images/workflow_2.jpg new file mode 100644 index 000000000..c918d5f15 Binary files /dev/null and b/docs/_images/workflow_2.jpg differ diff --git a/docs/_static/compas.css b/docs/_static/compas.css new file mode 100644 index 000000000..b5bb915d4 --- /dev/null +++ b/docs/_static/compas.css @@ -0,0 +1,30 @@ +html[data-theme="light"] { + --pst-color-primary: #0092d2; + --pst-color-info: #0092d2; + --pst-color-text-muted: #888; +} + +body { + line-height: 1.75; + font-weight: 300; +} + +.bd-article-container h1 { + color: #0092d2; +} + +.navbar-brand .logo__image { + height: 36px !important; +} + +.bd-header { + box-shadow: none; + border-bottom: 1px solid var(--pst-color-shadow); +} + +#rtd-footer-container { + height: 0px; + bottom: 0 !important; + margin: 0 !important; + display: none; +} diff --git a/docs/_static/compas.ico b/docs/_static/compas.ico new file mode 100644 index 000000000..ec6b1f27b Binary files /dev/null and b/docs/_static/compas.ico differ diff --git a/docs/_static/compas_icon.png b/docs/_static/compas_icon.png new file mode 100644 index 000000000..f112c7b3c Binary files /dev/null and b/docs/_static/compas_icon.png differ diff --git a/docs/_static/compas_icon_white.png b/docs/_static/compas_icon_white.png new file mode 100644 index 000000000..2c905bff9 Binary files /dev/null and b/docs/_static/compas_icon_white.png differ diff --git a/docs/_static/compas_white.ico b/docs/_static/compas_white.ico new file mode 100644 index 000000000..381bd9414 Binary files /dev/null and b/docs/_static/compas_white.ico differ diff --git a/docs/_static/versions.json b/docs/_static/versions.json new file mode 100644 index 000000000..d701ac20a --- /dev/null +++ b/docs/_static/versions.json @@ -0,0 +1,7 @@ +[ + { + "name": "latest", + "version": "unreleased", + "url": "https://compas.dev/compas_libigl/latest/" + } +] \ No newline at end of file diff --git a/docs/_templates/PLACEHOLDER b/docs/_templates/PLACEHOLDER deleted file mode 100644 index 27c4a9e40..000000000 --- a/docs/_templates/PLACEHOLDER +++ /dev/null @@ -1 +0,0 @@ -# template files for Sphinx diff --git a/docs/_templates/autosummary/base.rst b/docs/_templates/autosummary/base.rst deleted file mode 100644 index d7b3b9e54..000000000 --- a/docs/_templates/autosummary/base.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. rst-class:: detail - -{{ objname }} -{{ underline }} - -.. currentmodule:: {{ module }} - -.. auto{{ objtype }}:: {{ objname }} diff --git a/docs/_templates/autosummary/class.rst b/docs/_templates/autosummary/class.rst deleted file mode 100644 index 6a7bc1818..000000000 --- a/docs/_templates/autosummary/class.rst +++ /dev/null @@ -1,60 +0,0 @@ -.. rst-class:: detail - -{{ objname }} -{{ underline }} - -.. currentmodule:: {{ module }} - -.. autoclass:: {{ objname }} - - {% block attributes %} - {% if attributes %} - - .. rubric:: Attributes - - .. autosummary:: - {% for item in attributes %} - {%- if item not in inherited_members %} - ~{{ name }}.{{ item }} - {%- endif %} - {%- endfor %} - - .. rubric:: Inherited Attributes - - .. autosummary:: - {% for item in attributes %} - {%- if item in inherited_members %} - ~{{ name }}.{{ item }} - {%- endif %} - {%- endfor %} - - {% endif %} - {% endblock %} - - {% block methods %} - {% if methods %} - - .. rubric:: Methods - - .. autosummary:: - :toctree: - - {% for item in methods %} - {%- if item not in inherited_members %} - ~{{ name }}.{{ item }} - {%- endif %} - {%- endfor %} - - .. rubric:: Inherited Methods - - .. autosummary:: - :toctree: - - {% for item in methods %} - {%- if item in inherited_members %} - ~{{ name }}.{{ item }} - {%- endif %} - {%- endfor %} - - {% endif %} - {% endblock %} diff --git a/docs/_templates/autosummary/method.rst b/docs/_templates/autosummary/method.rst deleted file mode 100644 index d7b3b9e54..000000000 --- a/docs/_templates/autosummary/method.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. rst-class:: detail - -{{ objname }} -{{ underline }} - -.. currentmodule:: {{ module }} - -.. auto{{ objtype }}:: {{ objname }} diff --git a/docs/_templates/autosummary/module.rst b/docs/_templates/autosummary/module.rst deleted file mode 100644 index 6ca6bbfe9..000000000 --- a/docs/_templates/autosummary/module.rst +++ /dev/null @@ -1,39 +0,0 @@ -.. rst-class:: detail - -{{ fullname }} -{{ underline }} - -.. automodule:: {{ fullname }} - - {% block functions %} - {% if functions %} - .. rubric:: Functions - - .. autosummary:: - {% for item in functions %} - {{ item }} - {%- endfor %} - {% endif %} - {% endblock %} - - {% block classes %} - {% if classes %} - .. rubric:: Classes - - .. autosummary:: - {% for item in classes %} - {{ item }} - {%- endfor %} - {% endif %} - {% endblock %} - - {% block exceptions %} - {% if exceptions %} - .. rubric:: Exceptions - - .. autosummary:: - {% for item in exceptions %} - {{ item }} - {%- endfor %} - {% endif %} - {% endblock %} diff --git a/docs/_templates/sidebar-nav-bs.html b/docs/_templates/sidebar-nav-bs.html new file mode 100644 index 000000000..1051dc03b --- /dev/null +++ b/docs/_templates/sidebar-nav-bs.html @@ -0,0 +1,5 @@ + diff --git a/docs/api/compas_fea2.UI.rst b/docs/api/compas_fea2.UI.rst deleted file mode 100644 index ef258676c..000000000 --- a/docs/api/compas_fea2.UI.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_fea2.UI diff --git a/docs/api/compas_fea2.job.rst b/docs/api/compas_fea2.job.rst index 8d5719e38..05a1e9893 100644 --- a/docs/api/compas_fea2.job.rst +++ b/docs/api/compas_fea2.job.rst @@ -1,2 +1,11 @@ +******************************************************************************** +job +******************************************************************************** -.. automodule:: compas_fea2.job +.. currentmodule:: compas_fea2.job + +.. autosummary:: + :toctree: generated/ + + InputFile + ParametersFile diff --git a/docs/api/compas_fea2.model.rst b/docs/api/compas_fea2.model.rst index 2b843fa19..546320d94 100644 --- a/docs/api/compas_fea2.model.rst +++ b/docs/api/compas_fea2.model.rst @@ -1,2 +1,156 @@ +******************************************************************************** +model +******************************************************************************** -.. automodule:: compas_fea2.model +.. currentmodule:: compas_fea2.model + +Model +===== + +.. autosummary:: + :toctree: generated/ + + Model + +Parts +===== + +.. autosummary:: + :toctree: generated/ + + DeformablePart + RigidPart + +Nodes +===== + +.. autosummary:: + :toctree: generated/ + + Node + +Elements +======== + +.. autosummary:: + :toctree: generated/ + + Element + MassElement + BeamElement + SpringElement + TrussElement + StrutElement + TieElement + ShellElement + MembraneElement + Element3D + TetrahedronElement + HexahedronElement + +Releases +======== + +.. autosummary:: + :toctree: generated/ + + BeamEndRelease + BeamEndPinRelease + BeamEndSliderRelease + +Constraints +=========== + +.. autosummary:: + :toctree: generated/ + + Constraint + MultiPointConstraint + TieMPC + BeamMPC + TieConstraint + +Materials +========= + +.. autosummary:: + :toctree: generated/ + + Material + UserMaterial + Stiff + ElasticIsotropic + ElasticOrthotropic + ElasticPlastic + Concrete + ConcreteSmearedCrack + ConcreteDamagedPlasticity + Steel + Timber + +Sections +======== + +.. autosummary:: + :toctree: generated/ + + Section + BeamSection + SpringSection + AngleSection + BoxSection + CircularSection + HexSection + ISection + PipeSection + RectangularSection + ShellSection + MembraneSection + SolidSection + TrapezoidalSection + TrussSection + StrutSection + TieSection + MassSection + +Boundary Conditions +=================== + +.. autosummary:: + :toctree: generated/ + + BoundaryCondition + GeneralBC + FixedBC + PinnedBC + ClampBCXX + ClampBCYY + ClampBCZZ + RollerBCX + RollerBCY + RollerBCZ + RollerBCXY + RollerBCYZ + RollerBCXZ + +Initial Conditions +================== + +.. autosummary:: + :toctree: generated/ + + InitialCondition + InitialTemperatureField + InitialStressField + +Groups +====== + +.. autosummary:: + :toctree: generated/ + + Group + NodesGroup + ElementsGroup + FacesGroup + PartsGroup diff --git a/docs/devguide.rst b/docs/api/compas_fea2.postprocess.rst similarity index 54% rename from docs/devguide.rst rename to docs/api/compas_fea2.postprocess.rst index 9e833d9e5..f8d4ac498 100644 --- a/docs/devguide.rst +++ b/docs/api/compas_fea2.postprocess.rst @@ -1,11 +1,13 @@ ******************************************************************************** -Developer Guide +postprocess ******************************************************************************** -.. toctree:: - :maxdepth: 1 - :titlesonly: - :glob: +.. currentmodule:: compas_fea2.postprocess - devguide/overview +Stresses +======== +.. autosummary:: + :toctree: generated/ + + principal_stresses diff --git a/docs/api/compas_fea2.problem.rst b/docs/api/compas_fea2.problem.rst index 899fe9811..19f4027b5 100644 --- a/docs/api/compas_fea2.problem.rst +++ b/docs/api/compas_fea2.problem.rst @@ -1,2 +1,82 @@ +******************************************************************************** +problem +******************************************************************************** -.. automodule:: compas_fea2.problem +.. currentmodule:: compas_fea2.problem + +Problem +======= + +.. autosummary:: + :toctree: generated/ + + Problem + +Steps +===== + +.. autosummary:: + :toctree: generated/ + + Step + GeneralStep + Perturbation + ModalAnalysis + ComplexEigenValue + StaticStep + LinearStaticPerturbation + BucklingAnalysis + DynamicStep + QuasiStaticStep + DirectCyclicStep + +Prescribed Fields +================= + +.. autosummary:: + :toctree: generated/ + + PrescribedField + PrescribedTemperatureField + +Loads +===== + +.. autosummary:: + :toctree: generated/ + + Load + PrestressLoad + PointLoad + LineLoad + AreaLoad + GravityLoad + TributaryLoad + HarmonicPointLoad + HarmonicPressureLoad + ThermalLoad + +Displacements +============= + +.. autosummary:: + :toctree: generated/ + + GeneralDisplacement + +Load Patterns +============= + +.. autosummary:: + :toctree: generated/ + + Pattern + +Outputs +======= + +.. autosummary:: + :toctree: generated/ + + FieldOutput + HistoryOutput diff --git a/docs/api/compas_fea2.results.rst b/docs/api/compas_fea2.results.rst index 102f009fe..cc18f2bb9 100644 --- a/docs/api/compas_fea2.results.rst +++ b/docs/api/compas_fea2.results.rst @@ -1,2 +1,11 @@ +******************************************************************************** +results +******************************************************************************** -.. automodule:: compas_fea2.results +.. currentmodule:: compas_fea2.results + +.. autosummary:: + :toctree: generated/ + + Results + NodeFieldResults diff --git a/docs/api/compas_fea2.rst b/docs/api/compas_fea2.rst deleted file mode 100644 index dd29a37e2..000000000 --- a/docs/api/compas_fea2.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_fea2 diff --git a/docs/api/compas_fea2.units.rst b/docs/api/compas_fea2.units.rst index ab738c6ce..d4be026d2 100644 --- a/docs/api/compas_fea2.units.rst +++ b/docs/api/compas_fea2.units.rst @@ -1,2 +1,5 @@ +******************************************************************************** +Units +******************************************************************************** -.. automodule:: compas_fea2.units +compas_fe2 can use Pint for units consistency. diff --git a/docs/api/compas_fea2.utilities.rst b/docs/api/compas_fea2.utilities.rst index fb238248b..e6117aecf 100644 --- a/docs/api/compas_fea2.utilities.rst +++ b/docs/api/compas_fea2.utilities.rst @@ -1,2 +1,25 @@ +******************************************************************************** +Utilities +******************************************************************************** -.. automodule:: compas_fea2.utilities +.. currentmodule:: compas_fea2.utilities + + +Functions +========= + +.. autosummary:: + :toctree: generated/ + + colorbar + combine_all_sets + group_keys_by_attribute + group_keys_by_attributes + identify_ranges + mesh_from_shell_elements + network_order + normalise_data + principal_stresses + process_data + postprocess + plotvoxels diff --git a/docs/api/index.rst b/docs/api/index.rst new file mode 100644 index 000000000..fc7eedc92 --- /dev/null +++ b/docs/api/index.rst @@ -0,0 +1,16 @@ +******************************************************************************** +API Reference +******************************************************************************** + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + compas_fea2.job + compas_fea2.model + compas_fea2.postprocess + compas_fea2.problem + compas_fea2.results + compas_fea2.units + compas_fea2.utilities + diff --git a/docs/backends.rst b/docs/backends.rst deleted file mode 100644 index 11da3b52e..000000000 --- a/docs/backends.rst +++ /dev/null @@ -1,14 +0,0 @@ -******************************************************************************** -Backends -******************************************************************************** - -In the future, you will find here information about the supported backends... - -Abaqus -====== -.. toctree:: - :maxdepth: 1 - :titlesonly: - :glob: - - backends/** diff --git a/docs/api.rst b/docs/backends/index.rst similarity index 84% rename from docs/api.rst rename to docs/backends/index.rst index b70ed8161..f69972663 100644 --- a/docs/api.rst +++ b/docs/backends/index.rst @@ -1,8 +1,7 @@ ******************************************************************************** -API Reference +Backends ******************************************************************************** .. toctree:: :maxdepth: 2 - - api/compas_fea2 + :titlesonly: diff --git a/docs/conf.py b/docs/conf.py index 765d5f9c5..e00a83c1f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,56 +1,94 @@ +# flake8: noqa # -*- coding: utf-8 -*- # If your documentation needs a minimal Sphinx version, state it here. # -# needs_sphinx = '1.0' +# needs_sphinx = "1.0" -import sys -import os import inspect import importlib - -import sphinx_compas_theme -from sphinx.ext.napoleon.docstring import NumpyDocstring - -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../src')) +import re +import sphinx_compas_theme # this is a temp solution # -- General configuration ------------------------------------------------ -project = 'compas_fea2' -copyright = 'Block Research Group - ETH Zurich' -author = 'Francesco Ranaudo' -release = '0.1.0' -version = '.'.join(release.split('.')[0:2]) - -master_doc = 'index' -source_suffix = ['.rst', ] -templates_path = sphinx_compas_theme.get_autosummary_templates_path() -exclude_patterns = [] +project = "COMPAS FEA2" +copyright = "COMPAS Association" +author = "Francesco Ranaudo" +package = "compas_fea2" +organization = "compas-dev" + + +def get_latest_version(): + with open("../CHANGELOG.md", "r") as file: + content = file.read() + pattern = re.compile(r"## (Unreleased|\[\d+\.\d+\.\d+\])") + versions = pattern.findall(content) + latest_version = versions[0] if versions else None + if latest_version and latest_version.startswith("[") and latest_version.endswith("]"): + latest_version = latest_version[1:-1] + return latest_version + + +latest_version = get_latest_version() +if latest_version == "Unreleased": + release = "Unreleased" + version = "latest" +else: + release = latest_version + version = ".".join(release.split(".")[0:2]) # type: ignore + +master_doc = "index" +source_suffix = { + ".rst": "restructuredtext", + ".md": "markdown", +} +templates_path = sphinx_compas_theme.get_autosummary_templates_path() + ["_templates"] +exclude_patterns = [ + "_build", + "**.ipynb_checkpoints", + "_notebooks", + "**/__temp", + "**/__old", +] -pygments_style = 'sphinx' -show_authors = True add_module_names = True -language = 'en' +language = "en" # -- Extension configuration ------------------------------------------------ extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.mathjax', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinx.ext.githubpages', - 'matplotlib.sphinxext.plot_directive', - 'nbsphinx', - # 'sphinxcontrib.gist' + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.extlinks", + "sphinx.ext.githubpages", + "sphinx.ext.coverage", + "sphinx.ext.autodoc.typehints", + "sphinx_design", + "sphinx_inline_tabs", + "sphinx_togglebutton", + "sphinx_remove_toctrees", + "sphinx_copybutton", + "numpydoc", ] +numpydoc_show_class_members = False +numpydoc_class_members_toctree = False +numpydoc_attributes_as_param_list = True + # autodoc options +autodoc_type_aliases = {} + +# this does not work properly yet +# autodoc_typehints = "none" +# autodoc_typehints_format = "short" +autodoc_typehints_description_target = "documented" + autodoc_mock_imports = [ "System", "clr", @@ -61,21 +99,21 @@ "rhinoscriptsyntax", "bpy", "bmesh", - "mathutils" + "mathutils", ] -autodoc_default_flags = [ - 'undoc-members', - 'show-inheritance', -] +autodoc_default_options = { + "undoc-members": True, + "show-inheritance": True, +} -autodoc_member_order = 'alphabetical' +autodoc_member_order = "groupwise" -autoclass_content = 'class' +autoclass_content = "class" def skip(app, what, name, obj, would_skip, options): - if name.startswith('_'): + if name.startswith("_"): return True return would_skip @@ -83,45 +121,31 @@ def skip(app, what, name, obj, would_skip, options): def setup(app): app.connect("autodoc-skip-member", skip) -# autosummary options +# autosummary options autosummary_generate = True +autosummary_mock_imports = [ + "System", + "clr", + "Eto", + "Rhino", + "Grasshopper", + "scriptcontext", + "rhinoscriptsyntax", + "bpy", + "bmesh", + "mathutils", +] -# napoleon options +# graph options -napoleon_google_docstring = True -napoleon_numpy_docstring = True -napoleon_include_init_with_doc = False -napoleon_include_private_with_doc = True -napoleon_include_special_with_doc = True -napoleon_use_admonition_for_examples = False -napoleon_use_admonition_for_notes = False -napoleon_use_admonition_for_references = False -napoleon_use_ivar = False -napoleon_use_param = False -napoleon_use_rtype = False +# plot options +plot_include_source = False plot_html_show_source_link = False plot_html_show_formats = False - -# docstring sections - - -def parse_attributes_section(self, section): - return self._format_fields("Attributes", self._consume_fields()) - - -NumpyDocstring._parse_attributes_section = parse_attributes_section - - -def patched_parse(self): - self._sections["attributes"] = self._parse_attributes_section - self._unpatched_parse() - - -NumpyDocstring._unpatched_parse = NumpyDocstring._parse -NumpyDocstring._parse = patched_parse +plot_formats = ["png"] # intersphinx options @@ -134,71 +158,172 @@ def patched_parse(self): def linkcode_resolve(domain, info): - if domain != 'py': + if domain != "py": return None - if not info['module']: + if not info["module"]: return None - if not info['fullname']: + if not info["fullname"]: return None - package = info['module'].split('.')[0] - if not package.startswith('compas_fea2'): + package = info["module"].split(".")[0] + if not package.startswith(package): return None - module = importlib.import_module(info['module']) - parts = info['fullname'].split('.') + module = importlib.import_module(info["module"]) + parts = info["fullname"].split(".") if len(parts) == 1: - obj = getattr(module, info['fullname']) - filename = inspect.getmodule(obj).__name__.replace('.', '/') + obj = getattr(module, info["fullname"]) + mod = inspect.getmodule(obj) + if not mod: + return None + filename = mod.__name__.replace(".", "/") lineno = inspect.getsourcelines(obj)[1] elif len(parts) == 2: obj_name, attr_name = parts obj = getattr(module, obj_name) attr = getattr(obj, attr_name) if inspect.isfunction(attr): - filename = inspect.getmodule(obj).__name__.replace('.', '/') + mod = inspect.getmodule(attr) + if not mod: + return None + filename = mod.__name__.replace(".", "/") lineno = inspect.getsourcelines(attr)[1] else: return None else: return None - return f"https://github.com/compas-dev/compas_fea2/blob/master/src/{filename}.py#L{lineno}" + return f"https://github.com/{organization}/{package}/blob/main/src/{filename}.py#L{lineno}" + # extlinks -extlinks = {} +extlinks = { + "rhino": ("https://developer.rhino3d.com/api/RhinoCommon/html/T_%s.htm", "%s"), + "blender": ("https://docs.blender.org/api/2.93/%s.html", "%s"), +} -# intersphinx options +# from pytorch + +from sphinx.writers import html, html5 + + +def replace(Klass): + old_call = Klass.visit_reference + + def visit_reference(self, node): + if "refuri" in node: + refuri = node.get("refuri") + if "generated" in refuri: + href_anchor = refuri.split("#") + if len(href_anchor) > 1: + href = href_anchor[0] + anchor = href_anchor[1] + page = href.split("/")[-1] + parts = page.split(".") + if parts[-1] == "html": + pagename = ".".join(parts[:-1]) + if anchor == pagename: + node["refuri"] = href + return old_call(self, node) + + Klass.visit_reference = visit_reference -intersphinx_mapping = { - 'python': ('https://docs.python.org/', None), - 'compas': ('https://compas-dev.github.io/main', 'https://compas-dev.github.io/main/objects.inv'), -} +replace(html.HTMLTranslator) +replace(html5.HTML5Translator) # -- Options for HTML output ---------------------------------------------- -html_theme = 'compaspkg' -html_theme_path = sphinx_compas_theme.get_html_theme_path() +html_theme = "pydata_sphinx_theme" +html_logo = "_static/compas_icon.png" +html_title = project +html_favicon = "_static/compas.ico" html_theme_options = { - 'package_name': 'compas_fea2', - 'package_title': project, - 'package_version': release, - "package_author": "Francesco Ranaudo", - "package_docs": "https://compas-dev.github.io/compas_fea2/", - "package_repo": "https://github.com/compas-dev/compas_fea2", - "package_old_versions_txt": "https://compas-dev.github.io/compas_fea2/doc_versions.txt" + "logo": { + "text": project, + "image_light": "_static/compas_icon.png", + "image_dark": "_static/compas_icon_white.png", + }, + "switcher": { + "json_url": f"https://raw.githubusercontent.com/{organization}/{package}/gh-pages/versions.json", + "version_match": version, + }, + "check_switcher": False, + "navigation_depth": 3, + "show_nav_level": 1, + "show_toc_level": 2, + "pygment_light_style": "default", + "pygment_dark_style": "monokai", +} + +html_theme_options["external_links"]: [ + {"name": "COMPAS Framework", "url": "https://compas.dev"}, +] + +html_theme_options["icon_links"] = [ + { + "name": "GitHub", + "url": f"https://github.com/{organization}/{package}", + "icon": "fa-brands fa-github", + "type": "fontawesome", + }, + { + "name": "Discourse", + "url": "http://forum.compas-framework.org/", + "icon": "fa-brands fa-discourse", + "type": "fontawesome", + }, + { + "name": "PyPI", + "url": f"https://pypi.org/project/{package}/", + "icon": "fa-brands fa-python", + "type": "fontawesome", + }, +] + +html_theme_options["navbar_start"] = [ + "navbar-logo", +] + +html_theme_options["navbar_end"] = [ + "version-switcher", + "theme-switcher", + "navbar-icon-links", +] + +html_theme_options["navbar_persistent"] = ["search-button"] +html_theme_options["navbar_align"] = "content" +html_theme_options["secondary_sidebar_items"] = [ + "page-toc", + "edit-this-page", + "sourcelink", +] + +html_sidebars = { + "**": [ + "sidebar-nav-bs.html", + ] +} + +html_context = { + "github_url": "https://github.com", + "github_user": organization, + "github_repo": package, + "github_version": "main", + "doc_path": "docs", + "default_theme": "light", } -html_context = {} -html_static_path = sphinx_compas_theme.get_html_static_path() +html_static_path = ["_static"] +html_css_files = ["compas.css"] html_extra_path = [] html_last_updated_fmt = "" html_copy_source = False -html_show_sourcelink = False +html_show_sourcelink = True html_permalinks = False +html_permalinks_icon = "" html_compact_lists = True diff --git a/docs/tutorial.rst b/docs/development/index.rst similarity index 72% rename from docs/tutorial.rst rename to docs/development/index.rst index e6ca31ca5..a63ad37bd 100644 --- a/docs/tutorial.rst +++ b/docs/development/index.rst @@ -1,11 +1,7 @@ ******************************************************************************** -Tutorial +Development ******************************************************************************** .. toctree:: - :maxdepth: 1 - :titlesonly: - :glob: - - tutorial/** - + :maxdepth: 2 + :titlesonly: diff --git a/docs/devguide/overview.rst b/docs/devguide/overview.rst deleted file mode 100644 index fd557dec6..000000000 --- a/docs/devguide/overview.rst +++ /dev/null @@ -1,137 +0,0 @@ -******************************************************************************** -Overview -******************************************************************************** - -Some light reading :) - -https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging -https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes - -https://git-scm.com/docs/git-pull -https://git-scm.com/docs/git-rebase - - -Fork the repo -============= - -All contributions have to be made via pull requests from a fork of the repo. -To fork the ``compas_fea2`` repo, go to GitHub, click the "Fork" button -and select an account to host the fork. - - -Clone the fork -============== - -.. code-block:: bash - - git clone https://github.com//compas_fea2.git - - -Create a dev environment -======================== - -The recommended way to set up a development environment is with ``conda``. -Make sure to activate the environment before using it... - -.. code-block:: bash - - conda create -n fea2-dev python=3.8 --yes - conda activate fea2 - - -Install the requirements -======================== - -.. code-block:: bash - - pip install -r requirements-dev.txt - -.. note:: - - Note that this will also install ``compas`` and ``openseespy``, - and add an editable install of your fork of ``compas_fea2`` to the environment. - - -Run all checks and tests -======================== - -Before starting to work on your contribution, -it is generally a good idea to run all tests and checks -to make sure you have a healthy clone of the repo. - -.. code-block:: bash - - python -m compas_fea2.test - -.. note:: - - Note that the testing framework is currently not available yet. - - -Create a branch for your contribution -===================================== - -Create and ``checkout`` a new branch on the forked repo. - -.. code-block:: bash - - git branch my-awesome-contribution - git checkout my-awesome-contribution - - -Start making changes -==================== - -This is all you! - -Make sure to commit your changes regularly. -This makes it easier to undo if you change your mind about something... - -Also push the commits regularly to your remote fork. -This way you have plenty of backups in case your computer blows up :) - -.. code-block:: bash - - git commit -a -m "Some meaningful description of awesomeness" - - -Rebase on latest master/main -============================ - -Once you are done, the process of merging your contribution -into ``compas_fea2`` is much simpler if you rebase the contribution branch -of your fork onto the main branch of ``compas_fea2`` before submitting the PR. - -.. note:: - - This is a lot simpler using a Git GUI Client such as - SourceTree, SmartGit or GitKraken than on the command line... - - -Run all checks and tests -======================== - -Before pushing your local fork branch to the remote fork repo -make sure all tests and check still pass -and make changes if necessary. - -.. code-block:: bash - - python -m compas_fea2.test - -.. note:: - - Note that the testing framework is currently not available yet. - - -Push to remote fork -=================== - -Once all your changes have been commited, -the contribution bracnh is rebased onto the main branch of ``compaS_fea2``, -and all tests and checks pass, -push the local branch to the remote fork. - -.. code-block:: bash - - git push diff --git a/docs/index.rst b/docs/index.rst index ffae96d13..749882df8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,15 +1,59 @@ +:html_theme.sidebar_secondary.remove: + ******************************************************************************** -compas_fea2 +COMPAS FEA2 Documentation ******************************************************************************** +.. rst-class:: lead + +COMPAS FEA2 is a framework for Finite Element Analysis (FEA) written in Python. +It provides a high-level interface to various open-source and commercial FEA software, +with a unified API that is easy to use and extend. + + +User Guide +========== + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + userguide/index + + +API Reference +============= + .. toctree:: - :maxdepth: 3 - :titlesonly: + :maxdepth: 2 + :titlesonly: + + api/index + + +Backends +======== + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + backends/index + + +Development +=========== + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + development/index + + +Indices and tables +================== - intro - gettingstarted - backends - tutorial - api - devguide - license +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/tutorial/umat.rst b/docs/tutorial/umat.rst deleted file mode 100644 index 77297df42..000000000 --- a/docs/tutorial/umat.rst +++ /dev/null @@ -1,8 +0,0 @@ -******************************************************************************** -User Material subroutines -******************************************************************************** - -In case you want to use user material subroutines in Abaqus you can follow the following guide: -(original source: https://gist.github.com/franaudo/72362784ded685e4cb381e57020c9ec7) - -.. gist:: https://gist.github.com/franaudo/72362784ded685e4cb381e57020c9ec7 \ No newline at end of file diff --git a/docs/intro.rst b/docs/userguide/__old/gettingstarted.intro.rst similarity index 79% rename from docs/intro.rst rename to docs/userguide/__old/gettingstarted.intro.rst index a6eaf4be2..7d6e23729 100644 --- a/docs/intro.rst +++ b/docs/userguide/__old/gettingstarted.intro.rst @@ -2,8 +2,6 @@ Introduction ******************************************************************************** -.. rst-class:: lead - Plug-in architecture ==================== @@ -15,9 +13,9 @@ is complente, the results are recorded in a SQL database and can be accessed by the user through the SQL wrapper provided by ``compas_fea2``, by his/her own SQL statements or through an external interface. -.. figure:: /_images/registration.jpg - :figclass: figure - :class: figure-img img-fluid +.. .. figure:: /_images/registration.jpg +.. :figclass: figure +.. :class: figure-img img-fluid Workflow @@ -25,9 +23,10 @@ Workflow The image below describes a general FEA workflow: -.. figure:: /_images/workflow_1.png - :figclass: figure - :class: figure-img img-fluid +.. .. figure:: /_images/basic_workflow.png +.. :figclass: figure +.. :class: figure-img img-fluid + Collaboration Workflow ====================== @@ -42,19 +41,19 @@ of a structural engineer using rhino and abaqus collaborating with an acoustic engineer using blender and ansys: -.. figure:: /_images/CollaborationWorkflow.jpg - :figclass: figure - :class: figure-img img-fluid - +.. .. figure:: /_images/CollaborationWorkflow.jpg +.. :figclass: figure +.. :class: figure-img img-fluid -.. figure:: /_images/CollaborationWorkflow_example.jpg - :figclass: figure - :class: figure-img img-fluid +.. .. figure:: /_images/CollaborationWorkflow_example.jpg +.. :figclass: figure +.. :class: figure-img img-fluid Units ===== + Before starting any model, you need to decide which system of units you will use. ``compas_fea2`` has no built-in system of units. @@ -66,20 +65,19 @@ units you will use. ``compas_fea2`` has no built-in system of units. Some common systems of consistent units are shown in the table below: - .. csv-table:: Consistent Units - :file: ../data/units_consistent.csv + :file: units_consistent.csv :header-rows: 1 In case you do not want to follow a predefined system, you need to be consistent with your units assignemnts. Below there are some exmple of correct choices of units: .. csv-table:: Consistent Units - :file: ../data/units_consistent_2.csv + :file: units_consistent_2.csv :header-rows: 1 The order of magnitude expected for different properties is shown below: .. csv-table:: Magnitude - :file: ../data/units_magnitude.csv + :file: units_magnitude.csv :header-rows: 1 diff --git a/docs/userguide/acknowledgements.rst b/docs/userguide/acknowledgements.rst new file mode 100644 index 000000000..bda26ed4e --- /dev/null +++ b/docs/userguide/acknowledgements.rst @@ -0,0 +1,3 @@ +****************************************************************************** +Acknowledgements +****************************************************************************** diff --git a/docs/userguide/basics.analysis.rst b/docs/userguide/basics.analysis.rst new file mode 100644 index 000000000..774d26cf1 --- /dev/null +++ b/docs/userguide/basics.analysis.rst @@ -0,0 +1,3 @@ +****************************************************************************** +Analysis +****************************************************************************** diff --git a/docs/userguide/basics.data.rst b/docs/userguide/basics.data.rst new file mode 100644 index 000000000..9c3dc7dc3 --- /dev/null +++ b/docs/userguide/basics.data.rst @@ -0,0 +1,72 @@ +****************************************************************************** +Data +****************************************************************************** + +An analysis with COMPAS FEA2 is defined by a "model" (:class:`compas_fea2.model.Model`) +and a "problem" (:class:`compas_fea2.problem.Problem`), with each many different sub-components. + +All these components, and the model and problem themselves, are COMPAS data objects, +and derive from a base FEA2 data class (:class:`compas_fea2.base.FEAData`). + +.. code-block:: None + + compas.data.Data + |_ compas_fea2.base.FEAData + |_ compas_fea2.model.Model + |_ compas_fea2.model.Node + |_ compas_fea2.model.Element + |_ ... + |_ compas_fea2.model.Part + |_ ... + |_ compas_fea2.model.Material + |_ ... + |_ compas_fea2.model.Section + |_ ... + |_ compas_fea2.model.Constraint + |_ ... + |_ compas_fea2.model.Group + |_ ... + |_ compas_fea2.model.BoundaryCondition + |_ ... + |_ compas_fea2.model.InitialCondition + |_ ... + + +.. code-block:: None + + compas.data.Data + |_ compas_fea2.base.FEAData + |_ compas_fea2.problem.Problem + |_ compas_fea2.problem.Step + |_ ... + |_ compas_fea2.problem.Load + |_ ... + |_ compas_fea2.problem.Displacement + |_ ... + + +This means that all these components have the same base data infrastructure as all other COMPAS objects. +They have a guid, a name, and general attributes. + +>>> from compas_fea2.model import Node +>>> node = Node(name='node') +>>> node.name +'node' +>>> node.guid +... +>>> node.attributes +{} + +The can be converted to data and serialised to a JSON string or file. + +>>> node.to_data() +{'name': 'node', 'guid': ..., 'attributes': {}} +>>> node.to_jsonstring() +'{"name": "node", "guid": ..., "attributes": {}}' + +The only difference from other COMPAS objects is their default name. + +>>> node = Node() +>>> node.name +... + diff --git a/docs/userguide/basics.model.rst b/docs/userguide/basics.model.rst new file mode 100644 index 000000000..270c12f60 --- /dev/null +++ b/docs/userguide/basics.model.rst @@ -0,0 +1,63 @@ +****************************************************************************** +Model +****************************************************************************** + +At the heart of every COMPAS FEA2 analysis or simluation is a model. +A model consists of nodes, elements and parts, +and defines connections, constraints and boundary conditions. + +>>> from compas_fea2.model import Model +>>> model = Model() +>>> model + +Nodes +===== + +Nodes are the basic building blocks of a model. +They define the locations in space that define all other entities. + +>>> from compas_fea2.model import Node +>>> node = Node(x=0, y=0, z=0) +>>> node +Node(...) +>>> node.x +0.0 +>>> node.y +0.0 +>>> node.z +0.0 +>>> node.xyz +[0.0, 0.0, 0.0] +>>> node.point +Point(0.0, 0.0, 0.0) + +Besides coordinates, nodes have many other (optional) attributes. + +>>> node.mass +[None, None, None] +>>> node.temperature +None +>>> node.dof +{'x': True, 'y': True, 'z': True, 'xx': True, 'yy': True, 'zz': True} +>>> node.loads +{} +>>> node.displacements +{} + +Nodes also have a container for storing calculation results. + +>>> node.results +{} + + +Elements +======== + +Elements are defined by the nodes they connect to and a section. + + +>>> + + +Parts +===== diff --git a/docs/userguide/basics.overview.rst b/docs/userguide/basics.overview.rst new file mode 100644 index 000000000..9054e40c1 --- /dev/null +++ b/docs/userguide/basics.overview.rst @@ -0,0 +1,3 @@ +****************************************************************************** +Overview +****************************************************************************** diff --git a/docs/userguide/basics.problem.rst b/docs/userguide/basics.problem.rst new file mode 100644 index 000000000..bbca0b029 --- /dev/null +++ b/docs/userguide/basics.problem.rst @@ -0,0 +1,3 @@ +****************************************************************************** +Problem +****************************************************************************** diff --git a/docs/userguide/basics.results.rst b/docs/userguide/basics.results.rst new file mode 100644 index 000000000..5d4df3d75 --- /dev/null +++ b/docs/userguide/basics.results.rst @@ -0,0 +1,3 @@ +****************************************************************************** +Results +****************************************************************************** diff --git a/docs/userguide/basics.visualisation.rst b/docs/userguide/basics.visualisation.rst new file mode 100644 index 000000000..30bea1911 --- /dev/null +++ b/docs/userguide/basics.visualisation.rst @@ -0,0 +1,3 @@ +****************************************************************************** +Visualisation +****************************************************************************** diff --git a/docs/gettingstarted.rst b/docs/userguide/gettingstarted.installation.rst similarity index 52% rename from docs/gettingstarted.rst rename to docs/userguide/gettingstarted.installation.rst index 29bf7c7d8..b1582cb3a 100644 --- a/docs/gettingstarted.rst +++ b/docs/userguide/gettingstarted.installation.rst @@ -1,21 +1,6 @@ ******************************************************************************** -Getting Started -******************************************************************************** - -Requirements -============ - -To use ``compas_fea2`` to run analyses must have at least one -of the supported backends installed. - -* Abaqus -* ANSYS -* SOFiSTiK -* OpenSEES - - Installation -============ +******************************************************************************** The recommended way to install ``compas_fea2`` is in in a dedicated ``conda`` environment. @@ -32,21 +17,3 @@ to verify that you have a functional setup with at least one working backend. .. code-block:: bash python -m compas_fea2.test - - -First steps -=========== - -The tutorial and examples are a good place to start exploring. - -* Tutorial -* Examples - - -Known issues -============ - -Currently none :) - -If you do find problems, help us solving them by filing a bug report -on the `Issue Tracker `_ of the repo. diff --git a/docs/backends/backends.rst b/docs/userguide/gettingstarted.intro.rst similarity index 92% rename from docs/backends/backends.rst rename to docs/userguide/gettingstarted.intro.rst index 5d0eaa672..de27830af 100644 --- a/docs/backends/backends.rst +++ b/docs/userguide/gettingstarted.intro.rst @@ -1,5 +1,4 @@ ******************************************************************************** -Abaqus +Introduction ******************************************************************************** -Abaqus diff --git a/src/compas_fea2/cli/__init__.py b/docs/userguide/gettingstarted.nextsteps.rst similarity index 65% rename from src/compas_fea2/cli/__init__.py rename to docs/userguide/gettingstarted.nextsteps.rst index 174b9a64e..51a745e27 100644 --- a/src/compas_fea2/cli/__init__.py +++ b/docs/userguide/gettingstarted.nextsteps.rst @@ -1,9 +1,4 @@ -""" ******************************************************************************** -Command Line Interface +Next Steps ******************************************************************************** -.. currentmodule:: compas_fea2.cli -""" - -from .cli import * diff --git a/docs/userguide/gettingstarted.requirements.rst b/docs/userguide/gettingstarted.requirements.rst new file mode 100644 index 000000000..6c9977fd4 --- /dev/null +++ b/docs/userguide/gettingstarted.requirements.rst @@ -0,0 +1,17 @@ +******************************************************************************** +Requirements +******************************************************************************** + +COMPAS FEA2 is a high-level modelling language for finite element analysis. +It uses COMPAS data structures and geometry to define analysis models and related analysis problem definitions. +The actual analysis is handed off to open source solvers, such as OpenSEES or commercial analysis software, such as Abaqus. + +Currently the following solvers or "backends" are supported: + +* Abaqus +* ANSYS +* SOFiSTiK +* OpenSEES + +In order to run an analysis, you need to have one of these solvers installed on your system. +See :doc:`backends/index` for more information. diff --git a/docs/userguide/index.rst b/docs/userguide/index.rst new file mode 100644 index 000000000..e11b9d525 --- /dev/null +++ b/docs/userguide/index.rst @@ -0,0 +1,34 @@ +******************************************************************************** +User Guide +******************************************************************************** + +.. toctree:: + :maxdepth: 2 + :titlesonly: + :caption: Getting Started + + gettingstarted.intro + gettingstarted.requirements + gettingstarted.installation + gettingstarted.nextsteps + +.. toctree:: + :maxdepth: 2 + :titlesonly: + :caption: Tutorial + + basics.overview + basics.data + basics.model + basics.problem + basics.analysis + basics.results + basics.visualisation + +.. toctree:: + :maxdepth: 2 + :titlesonly: + :caption: Miscellaneous + + license + acknowledgements diff --git a/docs/license.rst b/docs/userguide/license.rst similarity index 83% rename from docs/license.rst rename to docs/userguide/license.rst index e6a80ce01..d895a30f5 100644 --- a/docs/license.rst +++ b/docs/userguide/license.rst @@ -2,4 +2,4 @@ License ******************************************************************************** -.. literalinclude:: ../LICENSE +.. literalinclude:: ../../LICENSE diff --git a/data/units_consistent.csv b/docs/userguide/units_consistent.csv similarity index 100% rename from data/units_consistent.csv rename to docs/userguide/units_consistent.csv diff --git a/data/units_consistent_2.csv b/docs/userguide/units_consistent_2.csv similarity index 100% rename from data/units_consistent_2.csv rename to docs/userguide/units_consistent_2.csv diff --git a/data/units_magnitude.csv b/docs/userguide/units_magnitude.csv similarity index 100% rename from data/units_magnitude.csv rename to docs/userguide/units_magnitude.csv diff --git a/environment.yml b/environment.yml new file mode 100644 index 000000000..06d355e6f --- /dev/null +++ b/environment.yml @@ -0,0 +1,9 @@ +name: fea2-dev +channels: + - conda-forge +dependencies: + - python>=3.8 + - pip>=19.0 + - compas + - pip: + - -r requirements-dev.txt diff --git a/pyproject.toml b/pyproject.toml index fd052f9be..6fe73ce2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,13 +3,10 @@ line-length = 120 [tool.pytest.ini_options] minversion = "6.0" -testpaths = ["tests", "src/compas_fea2"] -python_files = [ - "test_*.py", - "tests.py" -] +testpaths = ["tests"] +python_files = ["test_*.py", "tests.py"] addopts = "-ra --strict --doctest-modules --doctest-glob=*.rst --tb=short" -doctest_optionflags= "NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL ALLOW_UNICODE ALLOW_BYTES NUMBER" +doctest_optionflags = "NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL ALLOW_UNICODE ALLOW_BYTES NUMBER" filterwarnings = "ignore::DeprecationWarning" [tool.isort] @@ -24,4 +21,3 @@ known_first_party = "compas_fea2" default_section = "THIRDPARTY" forced_separate = "test_compas_fea2" skip = ["__init__.py"] - diff --git a/requirements-dev.txt b/requirements-dev.txt index 12c09d678..c939470f4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,36 +1,27 @@ attrs >=17.4 -autopep8 -black +black ==24.3.0 bump2version >=1.0.1 check-manifest >=0.36 compas_invocations doc8 flake8 -graphviz invoke >=0.14 -ipykernel -ipython >=5.8 isort -m2r2 -matplotlib -nbsphinx +jinja2 >= 3.0 +numpydoc +pydata-sphinx-theme pydocstyle -pytest >=3.2 -sphinx >=3.4 +pytest +pytest-mock +sphinx ==4.5 sphinx_compas_theme >=0.15.18 +sphinx-design +sphinx-inline-tabs +sphinx-togglebutton +sphinx-remove-toctrees +sphinx-copybutton +sphinxcontrib-bibtex +sphinxcontrib-youtube twine wheel - -# sphinxcontrib.gist - -# fea2_extensions -compas>=1.0 -compas_gmsh -Click -pint -python-dotenv -sqlalchemy - -e . - - diff --git a/requirements.txt b/requirements.txt index 258b23d5e..4ba80d7e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,4 @@ Click pint python-dotenv sqlalchemy==1.4 +openseespy diff --git a/setup.cfg b/setup.cfg index a443ffd9b..51298cc1b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,12 +2,41 @@ universal = 1 [flake8] -max-line-length = 120 +max-line-length = 180 exclude = */migrations/* [doc8] -max-line-length = 120 +max-line-length = 180 ignore = D001 [pydocstyle] convention = numpy + +[tool:pytest] +testpaths = + tests +norecursedirs = + migrations +python_files = + test_*.py + *_test.py + tests.py +addopts = + -ra + --strict + --doctest-glob=\*.rst + --tb=short +doctest_optionflags = + NORMALIZE_WHITESPACE + IGNORE_EXCEPTION_DETAIL + ALLOW_UNICODE + ALLOW_BYTES + NUMBER + +[isort] +force_single_line = True +line_length = 180 +known_first_party = compas_fea2 +default_section = THIRDPARTY +forced_separate = test_compas_fea2 +skip = migrations, __init__.py diff --git a/setup.py b/setup.py index 113a6930b..1a4191226 100644 --- a/setup.py +++ b/setup.py @@ -16,55 +16,51 @@ def read(*names, **kwargs): - return io.open( - path.join(here, *names), - encoding=kwargs.get('encoding', 'utf8') - ).read() + return io.open(path.join(here, *names), encoding=kwargs.get("encoding", "utf8")).read() -long_description = read('README.md') -requirements = read('requirements.txt').split('\n') +long_description = read("README.md") +requirements = read("requirements.txt").split("\n") optional_requirements = {} setup( - name='compas_fea2', - version='0.1.0', - description='2nd generation of compas_fea', + name="compas_fea2", + version="0.1.0", + description="2nd generation of compas_fea", long_description=long_description, - long_description_content_type='text/markdown', - url='https://github.com/fea2/compas_fea2', - author='Francesco Ranaudo', - author_email='ranaudo@arch.ethz.ch', - license='MIT license', + long_description_content_type="text/markdown", + url="https://github.com/fea2/compas_fea2", + author="Francesco Ranaudo", + author_email="ranaudo@arch.ethz.ch", + license="MIT license", classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'Topic :: Scientific/Engineering', - 'License :: OSI Approved :: MIT License', - 'Operating System :: Unix', - 'Operating System :: POSIX', - 'Operating System :: Microsoft :: Windows', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: Implementation :: CPython', + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Topic :: Scientific/Engineering", + "License :: OSI Approved :: MIT License", + "Operating System :: Unix", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: Implementation :: CPython", ], keywords=[], project_urls={}, - packages=['compas_fea2'], - package_dir={'': 'src'}, + packages=["compas_fea2"], + package_dir={"": "src"}, package_data={}, data_files=[], include_package_data=True, zip_safe=False, install_requires=requirements, - python_requires='>=3.8', + python_requires=">=3.8", extras_require=optional_requirements, entry_points={ - 'console_scripts': [ - "fea2=compas_fea2.cli:main"], + "console_scripts": ["fea2=compas_fea2.cli:main"], }, ext_modules=[], ) diff --git a/src/compas_fea2/UI/__init__.py b/src/compas_fea2/UI/__init__.py index 6b421f33d..a435eef0a 100644 --- a/src/compas_fea2/UI/__init__.py +++ b/src/compas_fea2/UI/__init__.py @@ -1,20 +1,3 @@ -""" -******************************************************************************** -User Interfaces -******************************************************************************** - -.. currentmodule:: compas_fea2.UI - -compas_view2 -============ - -.. autosummary:: - :toctree: generated/ - - FEA2Viewer - -""" - from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -22,5 +5,5 @@ from .viewer import FEA2Viewer __all__ = [ - 'FEA2Viewer', + "FEA2Viewer", ] diff --git a/src/compas_fea2/UI/viewer/__init__.py b/src/compas_fea2/UI/viewer/__init__.py index 075025cdd..04a73d9e8 100644 --- a/src/compas_fea2/UI/viewer/__init__.py +++ b/src/compas_fea2/UI/viewer/__init__.py @@ -20,8 +20,8 @@ ) __all__ = [ - 'FEA2Viewer', - 'BCShape', - 'FixBCShape', - 'PinBCShape', + "FEA2Viewer", + "BCShape", + "FixBCShape", + "PinBCShape", ] diff --git a/src/compas_fea2/UI/viewer/shapes.py b/src/compas_fea2/UI/viewer/shapes.py index d90a04f9d..4ef0ace79 100644 --- a/src/compas_fea2/UI/viewer/shapes.py +++ b/src/compas_fea2/UI/viewer/shapes.py @@ -1,10 +1,9 @@ from compas.geometry import Cone -from compas.geometry import Circle -from compas.geometry import Plane from compas.geometry import Box +from compas.geometry import Frame -class BCShape(): +class BCShape: def __init__(self, xyz, direction, scale): self.x, self.y, self.z = xyz self.direction = direction @@ -14,20 +13,21 @@ def __init__(self, xyz, direction, scale): class PinBCShape(BCShape): def __init__(self, xyz, direction=[0, 0, 1], scale=1): super(PinBCShape, self).__init__(xyz, direction, scale) - self.height = 0.4*self.scale - self.diameter = 0.4*self.scale + self.height = 0.4 * self.scale + self.diameter = 0.4 * self.scale # FIXME this is wrong because it should follow the normal - self.plane = Plane([self.x, self.y, self.z-self.height], direction) - self.circle = Circle(self.plane, self.diameter) - self.shape = Cone(self.circle, self.height) + # self.plane = Plane([self.x, self.y, self.z - self.height], direction) + # self.circle = Circle(self.plane, self.diameter) + frame = Frame([self.x, self.y, self.z - self.height], [1, 0, 0], [0, 1, 0]) + self.shape = Cone(0.5 * self.diameter, self.height, frame=frame) class FixBCShape(BCShape): def __init__(self, xyz, scale=1): super(FixBCShape, self).__init__(xyz, [0, 0, 1], scale) - self.height = 0.8*self.scale - self.shape = Box(([self.x, self.y, self.z-self.height/4], [1, 0, 0], - [0, 1, 0]), self.height, self.height, self.height/2) + self.height = 0.8 * self.scale + frame = Frame([self.x, self.y, self.z - self.height], [1, 0, 0], [0, 1, 0]) + self.shape = Box(self.height, self.height, self.height / 2, frame=frame) class MomentShape(BCShape): diff --git a/src/compas_fea2/UI/viewer/viewer.py b/src/compas_fea2/UI/viewer/viewer.py index 876c2f15d..0c8483827 100644 --- a/src/compas_fea2/UI/viewer/viewer.py +++ b/src/compas_fea2/UI/viewer/viewer.py @@ -1,13 +1,10 @@ -from importlib.metadata import distribution from typing import Iterable from compas_view2.app import App from compas_view2.objects import Collection from compas_view2.shapes import Arrow -from compas_view2.collections import Collection from compas_view2.shapes import Text from compas.datastructures import Mesh -from compas.geometry import Scale from compas.geometry import Line from compas.geometry import Polyhedron from compas.geometry import Vector @@ -16,21 +13,19 @@ from compas_fea2.UI.viewer.shapes import PinBCShape from compas_fea2.UI.viewer.shapes import FixBCShape from compas_fea2.model.elements import ShellElement -from compas_fea2.model.elements import _Element3D +from compas_fea2.model.elements import Element3D from compas_fea2.model.elements import BeamElement from compas_fea2.model.bcs import FixedBC, PinnedBC from compas_fea2.problem.loads import PointLoad -from compas_fea2.problem.steps import _GeneralStep - -from compas_fea2.utilities._utils import _compute_model_dimensions +from compas_fea2.problem.steps import GeneralStep def hextorgb(hex): return tuple(i / 255 for i in hex_to_rgb(hex)) -class FEA2Viewer(): +class FEA2Viewer: """Wrapper for the compas_view2 viewer app. Parameters @@ -52,14 +47,14 @@ def __init__(self, width=800, height=500, **kwargs): self.height = height self.app = App(width=width, height=height) - sf = kwargs.get('scale_factor',1) + sf = kwargs.get("scale_factor", 1) - self.app.view.camera.target = [3000*sf, 3000*sf, 1000*sf] - self.app.view.camera.position = [7000*sf, 7000*sf, 5000*sf] - self.app.view.camera.near = 1*sf - self.app.view.camera.far = 100000*sf - self.app.view.camera.scale = 1000*sf - self.app.view.grid.cell_size = 1000*sf + self.app.view.camera.target = [3000 * sf, 3000 * sf, 1000 * sf] + self.app.view.camera.position = [7000 * sf, 7000 * sf, 5000 * sf] + self.app.view.camera.near = 1 * sf + self.app.view.camera.far = 100000 * sf + self.app.view.camera.scale = 1000 * sf + self.app.view.grid.cell_size = 1000 * sf def draw_mesh(self, mesh): self.app.add(mesh, use_vertex_color=True) @@ -84,7 +79,7 @@ def draw_parts(self, parts, draw_nodes=False, node_labels=False, solid=False): parts = parts if isinstance(parts, Iterable) else [parts] for part in parts: if solid: - self.draw_solid_elements(filter(lambda x: isinstance(x, _Element3D), part.elements), draw_nodes) + self.draw_solid_elements(filter(lambda x: isinstance(x, Element3D), part.elements), draw_nodes) else: if part.discretized_boundary_mesh: self.app.add(part.discretized_boundary_mesh, use_vertex_color=True) @@ -104,7 +99,7 @@ def draw_nodes(self, nodes, node_lables): If `True` add the nodes. """ pts = [node.point for node in nodes] - self.app.add(pts, colors=[hextorgb("#386641")]*len(pts)) + self.app.add(pts, colors=[hextorgb("#386641")] * len(pts)) for node in nodes: if node_lables: @@ -116,7 +111,7 @@ def draw_solid_elements(self, elements, show_vertices=True): Parameters ---------- - elements : :class:`compas_fea2.model.ShellElement` | :class:`compas_fea2.model._Element3D` | :class:`compas_fea2.model.BeamElement` + elements : :class:`compas_fea2.model.ShellElement` | :class:`compas_fea2.model.Element3D` | :class:`compas_fea2.model.BeamElement` _description_ show_vertices : bool, optional If `True` show the vertices of the elements, by default True @@ -127,14 +122,14 @@ def draw_solid_elements(self, elements, show_vertices=True): pts = [node.point for node in element.nodes] collection_items.append(Polyhedron(pts, list(element._face_indices.values()))) if collection_items: - self.app.add(Collection(collection_items), facecolor=(.9, .9, .9)) + self.app.add(Collection(collection_items), facecolor=(0.9, 0.9, 0.9)) def draw_shell_elements(self, elements, show_vertices=True): """Draw the elements of a part. Parameters ---------- - elements : :class:`compas_fea2.model.ShellElement` | :class:`compas_fea2.model._Element3D` | :class:`compas_fea2.model.BeamElement` + elements : :class:`compas_fea2.model.ShellElement` | :class:`compas_fea2.model.Element3D` | :class:`compas_fea2.model.BeamElement` _description_ show_vertices : bool, optional If `True` show the vertices of the elements, by default True @@ -150,7 +145,7 @@ def draw_shell_elements(self, elements, show_vertices=True): else: raise NotImplementedError("only 3 and 4 vertices shells supported at the moment") if collection_items: - self.app.add(Collection(collection_items), facecolor=(.9, .9, .9)) + self.app.add(Collection(collection_items), facecolor=(0.9, 0.9, 0.9)) def draw_beam_elements(self, elements, show_vertices=True): """Draw the elements of a part. @@ -170,7 +165,7 @@ def draw_beam_elements(self, elements, show_vertices=True): if collection_items: self.app.add(Collection(collection_items), linewidth=10) - def draw_bcs(self, model, parts=None, scale_factor=1.): + def draw_bcs(self, model, parts=None, scale_factor=1.0): """Draw the support boundary conditions. Parameters @@ -199,28 +194,32 @@ def draw_bcs(self, model, parts=None, scale_factor=1.): if bcs_collection: self.app.add(Collection(bcs_collection), facecolor=(1, 0, 0), opacity=0.5) - def draw_loads(self, step, scale_factor=1., app_point='end'): + def draw_loads(self, step, scale_factor=1.0, app_point="end"): """Draw the applied loads for given steps. Parameters ---------- - steps : [:class:`compas_fea2.problem._Step`] + steps : [:class:`compas_fea2.problem.Step`] List of steps. Only the loads in these steps will be shown. scale_factor : float, optional Scale the loads reppresentation to have a nicer drawing, by default 1. """ - if isinstance(step, _GeneralStep): + if isinstance(step, GeneralStep): for pattern in step._patterns: if isinstance(pattern.load, PointLoad): - vector = Vector(x=pattern.load.components['x'] or 0., - y=pattern.load.components['y'] or 0., - z=pattern.load.components['z'] or 0.) + vector = Vector( + x=pattern.load.components["x"] or 0.0, + y=pattern.load.components["y"] or 0.0, + z=pattern.load.components["z"] or 0.0, + ) if vector.length == 0: continue vector.scale(scale_factor) - if app_point=='end': - pts = [[node.x-vector.x, node.y-vector.y, node.z-vector.z] for node in pattern.distribution] + if app_point == "end": + pts = [ + [node.x - vector.x, node.y - vector.y, node.z - vector.z] for node in pattern.distribution + ] else: pts = [node.point for node in pattern.distribution] # TODO add moment components xx, yy, zz @@ -244,26 +243,22 @@ def draw_nodes_vector(self, pts, vectors, colors=None): arrows = [] arrows_properties = [] if not colors: - colors = [(0, 1, 0)]*len(pts) + colors = [(0, 1, 0)] * len(pts) for pt, vector, color in zip(pts, vectors, colors): - arrows.append(Arrow(pt, vector, - head_portion=0.3, head_width=0.15, body_width=0.05)) - arrows_properties.append({"u": 3, - "show_lines": False, - "facecolor": color}) + arrows.append(Arrow(pt, vector, head_portion=0.3, head_width=0.15, body_width=0.05)) + arrows_properties.append({"u": 3, "show_lines": False, "facecolor": color}) if arrows: self.app.add(Collection(arrows, arrows_properties)) def show(self): - """Display the viewport. - """ + """Display the viewport.""" self.app.show() def dynamic_show(self): - """Display the viewport dynamically. - """ + """Display the viewport dynamically.""" self.app.run() + # class BeamViewer(): # pass diff --git a/src/compas_fea2/__init__.py b/src/compas_fea2/__init__.py index bda49fe95..fe35de040 100644 --- a/src/compas_fea2/__init__.py +++ b/src/compas_fea2/__init__.py @@ -1,42 +1,8 @@ -""" -******************************************************************************** -compas_fea2 -******************************************************************************** - -.. currentmodule:: compas_fea2 - - -Core Packages -============= - -.. toctree:: - :maxdepth: 1 - - compas_fea2.model - compas_fea2.problem - compas_fea2.results - compas_fea2.job - compas_fea2.postprocess - compas_fea2.utilities - compas_fea2.units - -User Interfaces -=============== - -.. toctree:: - :maxdepth: 1 - - compas_fea2.cli - compas_fea2.UI - - -""" import os from collections import defaultdict - -import os from dotenv import load_dotenv + __author__ = ["Francesco Ranaudo"] __copyright__ = "Block Research Group" __license__ = "MIT License" @@ -52,9 +18,9 @@ DOCS = os.path.abspath(os.path.join(HOME, "docs")) TEMP = os.path.abspath(os.path.join(HOME, "temp")) -def init_fea2(verbose=False, point_overlap=True, global_tolerance=1, precision='3f'): - """Create a default environment file if it doesn't exist and loads its - variables. + +def init_fea2(verbose=False, point_overlap=True, global_tolerance=1, precision="3f"): + """Create a default environment file if it doesn't exist and loads its variables. Parameters ---------- @@ -66,32 +32,40 @@ def init_fea2(verbose=False, point_overlap=True, global_tolerance=1, precision=' Tolerance for the model, by default 1 precision : str, optional Values approximation, by default '3f' + """ env_path = os.path.abspath(os.path.join(HERE, ".env")) if not os.path.exists(env_path): with open(env_path, "x") as f: - f.write('\n'.join([ - "VERBOSE={}".format(verbose), - "POINT_OVERLAP={}".format(point_overlap), - "GLOBAL_TOLERANCE={}".format(point_overlap), - "PRECISION={}".format(precision) - ])) + f.write( + "\n".join( + [ + "VERBOSE={}".format(verbose), + "POINT_OVERLAP={}".format(point_overlap), + "GLOBAL_TOLERANCE={}".format(point_overlap), + "PRECISION={}".format(precision), + ] + ) + ) load_dotenv(env_path) + if not load_dotenv(): init_fea2() -VERBOSE = os.getenv('VERBOSE').lower() == 'true' -POINT_OVERLAP = os.getenv('POINT_OVERLAP').lower() == 'true' -GLOBAL_TOLERANCE = os.getenv('GLOBAL_TOLERANCE') -PRECISION = os.getenv('PRECISION') +VERBOSE = os.getenv("VERBOSE").lower() == "true" +POINT_OVERLAP = os.getenv("POINT_OVERLAP").lower() == "true" +GLOBAL_TOLERANCE = os.getenv("GLOBAL_TOLERANCE") +PRECISION = os.getenv("PRECISION") BACKEND = None BACKENDS = defaultdict(dict) + def set_precision(precision): global PRECISION PRECISION = precision + # pluggable function to be def _register_backend(): """Create the class registry for the plugin. @@ -103,6 +77,7 @@ def _register_backend(): """ raise NotImplementedError + def set_backend(plugin): """Set the backend plugin to be used. @@ -118,16 +93,17 @@ def set_backend(plugin): If the plugin library is not found. """ import importlib + global BACKEND BACKEND = plugin try: importlib.import_module(plugin)._register_backend() except ImportError: - print('backend plugin not found. Make sure that you have installed it before.') + print("backend plugin not found. Make sure that you have installed it before.") + def _get_backend_implementation(cls): return BACKENDS[BACKEND].get(cls) __all__ = ["HOME", "DATA", "DOCS", "TEMP"] - diff --git a/src/compas_fea2/base.py b/src/compas_fea2/base.py index 47de12fc6..01632301f 100644 --- a/src/compas_fea2/base.py +++ b/src/compas_fea2/base.py @@ -2,14 +2,14 @@ from __future__ import absolute_import from __future__ import division +from typing import Iterable from compas.data import Data import compas_fea2 import importlib -import uuid -from typing import Iterable from abc import abstractmethod + class FEAData(Data): """Base class for all FEA model objects. @@ -20,59 +20,62 @@ class FEAData(Data): in a model and/or problem summary, and for their representation in software-specific calculation files. - Examples - -------- - >>> + Parameters + ---------- + name : str, optional + The name of the object, by default None. If not provided, one is automatically + generated. - """ + Attributes + ---------- + name : str + The name of the object. + registration : compas_fea2 object + The mother object where this object is registered to. - def __init__(self, name=None, *args, **kwargs): - """Base class for all FEA2 objects. - - Parameters - ---------- - name : str, optional - The name of the object, by default None. If not provided, one is automatically - generated. - - Attributes - ---------- - name : str - The name of the object. - registration : compas_fea2 object - The mother object where this object is registered to. - """ - super().__init__() - # NOTE the names length in abaqus is limited to 80 characters - self.uid = uuid.uuid4() - self._name = name or ''.join([c for c in type(self).__name__ if c.isupper()])+"_"+str(id(self)) - self._registration = None + """ def __new__(cls, *args, **kwargs): - """Try to get the backend plug-in implementation, otherwise use the base - one. - """ imp = compas_fea2._get_backend_implementation(cls) if not imp: return super(FEAData, cls).__new__(cls) return super(FEAData, imp).__new__(imp) + def __init__(self, name=None, **kwargs): + super().__init__(name=name, **kwargs) + self._name = name or "".join([c for c in type(self).__name__ if c.isupper()]) + "_" + str(id(self)) + self._registration = None + def __repr__(self): - return '{0}({1})'.format(self.__class__.__name__, id(self)) + return "{0}({1})".format(self.__class__.__name__, id(self)) + + def __str__(self): + title = "compas_fea2 {0} object".format(self.__class__.__name__) + separator = "-" * (len(title)) + data_extended = [] + for a in list( + filter(lambda a: not a.startswith("__") and not a.startswith("_") and a != "jsondefinitions", dir(self)) + ): + try: + attr = getattr(self, a) + if not callable(attr): + if not isinstance(attr, Iterable): + data_extended.append("{0:<15} : {1}".format(a, attr.__repr__())) + else: + data_extended.append("{0:<15} : {1}".format(a, len(attr))) + except Exception: + pass + return """\n{}\n{}\n{}\n""".format(title, separator, "\n".join(data_extended)) @abstractmethod def jobdata(self, *args, **kwargs): """Generate the job data for the backend-specific input file.""" - raise NotImplementedError('This function is not available in the selected plugin.') + raise NotImplementedError("This function is not available in the selected plugin.") @classmethod def from_name(cls, name, **kwargs): """Create an instance of a class of the registered plugin from its name. - Note - ---- - By convention, only hidden class can be called by this method. - Parameters ---------- name : str @@ -82,53 +85,13 @@ def from_name(cls, name, **kwargs): ------- obj The wanted object - """ - obj = cls(**kwargs) - module_info = obj.__module__.split('.') - obj = getattr(importlib.import_module('.'.join([*module_info[:-1]])), '_'+name) - return obj(**kwargs) - - def data(self): - pass - - def __str__(self): - """String representation of the object. - - This method is used to explicitly convert the object to a string, with :func:``str``, - or implicitly, using the print function. - - Returns - ------- - str - - Examples - -------- - Convert the object to a string. - This returns a value. - >>> s = str(obj) - >>> s - '...' - - Print the object. - This does not return a value. + Notes + ----- + By convention, only hidden class can be called by this method. - >>> p = print(obj) - '...' - >>> p - None """ - title = 'compas_fea2 {0} object'.format(self.__class__.__name__) - separator = '-' * (len(title)) - data_extended = [] - for a in list(filter(lambda a: not a.startswith('__') and not a.startswith('_') and a != 'jsondefinitions', dir(self))): - try: - attr = getattr(self, a) - if not callable(attr): - if not isinstance(attr, Iterable): - data_extended.append('{0:<15} : {1}'.format(a, attr.__repr__())) - else: - data_extended.append('{0:<15} : {1}'.format(a, len(attr))) - except Exception: - pass - return """\n{}\n{}\n{}\n""".format(title, separator, '\n'.join(data_extended)) + obj = cls(**kwargs) + module_info = obj.__module__.split(".") + obj = getattr(importlib.import_module(".".join([*module_info[:-1]])), "_" + name) + return obj(**kwargs) diff --git a/src/compas_fea2/cli/cli.py b/src/compas_fea2/cli.py similarity index 75% rename from src/compas_fea2/cli/cli.py rename to src/compas_fea2/cli.py index b42b9048d..a03f4efa1 100644 --- a/src/compas_fea2/cli/cli.py +++ b/src/compas_fea2/cli.py @@ -4,12 +4,9 @@ import sys import os import click -from compas_fea2 import HOME - -import os import json -import sys +from compas_fea2 import HOME from fea2_extension.main import init_plugin @@ -31,8 +28,8 @@ def one_o_one(): @main.command() -@click.option('--clean', default='False', help='remove existing directories') -@click.argument('backend') +@click.option("--clean", default="False", help="remove existing directories") +@click.argument("backend") def init_backend(backend, clean): """Initialize a bare backend module.\n backend : txt\n @@ -41,11 +38,12 @@ def init_backend(backend, clean): init_plugin(HOME, backend, clean) backend = backend.lower() + @main.command() # @click.option('--clean', default='False', help='remove existing directories') -@click.argument('backend') -@click.argument('setting') -@click.argument('value') +@click.argument("backend") +@click.argument("setting") +@click.argument("value") def change_settings(backend, setting, value): """Change a setting for the specified backend.\n backend : txt\n @@ -55,15 +53,16 @@ def change_settings(backend, setting, value): value : txt\n The new value for the setting. """ - backend_settings = os.path.join(HOME, 'src', 'compas_fea2', 'backends', backend.lower(), 'settings.json') + backend_settings = os.path.join(HOME, "src", "compas_fea2", "backends", backend.lower(), "settings.json") - with open(backend_settings, 'r') as f: + with open(backend_settings, "r") as f: settings = json.load(f) - with open(backend_settings, 'w') as f: - settings[setting]=value + with open(backend_settings, "w") as f: + settings[setting] = value json.dump(settings, f) + # -------------------------------- DEBUG ----------------------------------# if __name__ == "__main__": sys.exit(main.init_backend()) diff --git a/src/compas_fea2/job/__init__.py b/src/compas_fea2/job/__init__.py index 21231b685..ba7b1af49 100644 --- a/src/compas_fea2/job/__init__.py +++ b/src/compas_fea2/job/__init__.py @@ -1,17 +1,3 @@ -""" -******************************************************************************** -job -******************************************************************************** - -.. currentmodule:: compas_fea2.job - -.. autosummary:: - :toctree: generated/ - - InputFile - ParametersFile - -""" from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -19,7 +5,4 @@ from .input_file import InputFile from .input_file import ParametersFile -__all__ = [ - 'InputFile', - 'ParametersFile' -] +__all__ = ["InputFile", "ParametersFile"] diff --git a/src/compas_fea2/job/input_file.py b/src/compas_fea2/job/input_file.py index 5b10e778d..cb422a90c 100644 --- a/src/compas_fea2/job/input_file.py +++ b/src/compas_fea2/job/input_file.py @@ -4,7 +4,6 @@ import os from compas_fea2.base import FEAData -from compas_fea2.utilities._utils import timer class InputFile(FEAData): @@ -27,6 +26,7 @@ class InputFile(FEAData): The model associated to the Problem. path : str Complete path to the input file. + """ def __init__(self, name=None, **kwargs): @@ -48,7 +48,6 @@ def model(self): def path(self): return self._path - @classmethod def from_problem(cls, problem): """Create an InputFile object from a :class:`compas_fea2.problem.Problem` @@ -62,17 +61,19 @@ def from_problem(cls, problem): ------- obj InputFile for the analysis. + """ input_file = cls() input_file._registration = problem input_file._job_name = problem._name - input_file._file_name = '{}.{}'.format(problem._name, input_file._extension) + input_file._file_name = "{}.{}".format(problem._name, input_file._extension) input_file._path = problem.path.joinpath(input_file._file_name) return input_file # ============================================================================== # General methods # ============================================================================== + def write_to_file(self, path=None): """Writes the InputFile to a file in a specified location. @@ -86,19 +87,20 @@ def write_to_file(self, path=None): ------- str Information about the results of the writing process. + """ path = path or self.problem.path if not path: - raise ValueError('A path to the folder for the input file must be provided') + raise ValueError("A path to the folder for the input file must be provided") file_path = os.path.join(path, self._file_name) - with open(file_path, 'w') as f: + with open(file_path, "w") as f: f.writelines(self.jobdata()) - print('Input file generated in: {}'.format(file_path)) + print("Input file generated in: {}".format(file_path)) class ParametersFile(InputFile): - """ - """ + """""" + def __init__(self, name=None, **kwargs): super(ParametersFile, self).__init__(name, **kwargs) raise NotImplementedError() diff --git a/src/compas_fea2/model/__init__.py b/src/compas_fea2/model/__init__.py index fb0dd2f42..ba037b780 100644 --- a/src/compas_fea2/model/__init__.py +++ b/src/compas_fea2/model/__init__.py @@ -1,162 +1,3 @@ -""" -******************************************************************************** -model -******************************************************************************** - -.. currentmodule:: compas_fea2.model - -Model -===== - -.. autosummary:: - :toctree: generated/ - - Model - -Parts -===== - -.. autosummary:: - :toctree: generated/ - - DeformablePart - RigidPart - -Nodes -===== - -.. autosummary:: - :toctree: generated/ - - Node - -Elements -======== - -.. autosummary:: - :toctree: generated/ - - _Element - MassElement - BeamElement - SpringElement - TrussElement - StrutElement - TieElement - ShellElement - MembraneElement - _Element3D - TetrahedronElement - HexahedronElement - -Releases -======== - -.. autosummary:: - :toctree: generated/ - - _BeamEndRelease - BeamEndPinRelease - BeamEndSliderRelease - -Constraints -=========== - -.. autosummary:: - :toctree: generated/ - - _Constraint - _MultiPointConstraint - TieMPC - BeamMPC - TieConstraint - -Materials -========= - -.. autosummary:: - :toctree: generated/ - - _Material - UserMaterial - Stiff - ElasticIsotropic - ElasticOrthotropic - ElasticPlastic - Concrete - ConcreteSmearedCrack - ConcreteDamagedPlasticity - Steel - Timber - -Sections -======== - -.. autosummary:: - :toctree: generated/ - - _Section - BeamSection - SpringSection - AngleSection - BoxSection - CircularSection - HexSection - ISection - PipeSection - RectangularSection - ShellSection - MembraneSection - SolidSection - TrapezoidalSection - TrussSection - StrutSection - TieSection - MassSection - -Boundary Conditions -=================== - -.. autosummary:: - :toctree: generated/ - - _BoundaryCondition - GeneralBC - FixedBC - PinnedBC - ClampBCXX - ClampBCYY - ClampBCZZ - RollerBCX - RollerBCY - RollerBCZ - RollerBCXY - RollerBCYZ - RollerBCXZ - -Initial Conditions -================== - -.. autosummary:: - :toctree: generated/ - - _InitialCondition - InitialTemperatureField - InitialStressField - -Groups -====== - -.. autosummary:: - :toctree: generated/ - - _Group - NodesGroup - ElementsGroup - FacesGroup - PartsGroup - -""" from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -168,7 +9,7 @@ ) from .nodes import Node from .elements import ( - _Element, + Element, MassElement, BeamElement, SpringElement, @@ -177,25 +18,27 @@ TieElement, ShellElement, MembraneElement, - _Element3D, + Element3D, TetrahedronElement, HexahedronElement, ) -from .materials import ( - _Material, - Concrete, - ConcreteSmearedCrack, - ConcreteDamagedPlasticity, +from .materials.material import ( + Material, ElasticIsotropic, - Stiff, - UserMaterial, ElasticOrthotropic, ElasticPlastic, - Steel, - Timber, + Stiff, + UserMaterial, +) +from .materials.concrete import ( + Concrete, + ConcreteDamagedPlasticity, + ConcreteSmearedCrack, ) +from .materials.steel import Steel +from .materials.timber import Timber from .sections import ( - _Section, + Section, MassSection, BeamSection, SpringSection, @@ -215,26 +58,26 @@ TieSection, ) from .constraints import ( - _Constraint, - _MultiPointConstraint, + Constraint, + MultiPointConstraint, TieMPC, BeamMPC, TieConstraint, ) from .groups import ( - _Group, + Group, NodesGroup, ElementsGroup, FacesGroup, PartsGroup, ) from .releases import ( - _BeamEndRelease, + BeamEndRelease, BeamEndPinRelease, BeamEndSliderRelease, ) from .bcs import ( - _BoundaryCondition, + BoundaryCondition, GeneralBC, FixedBC, PinnedBC, @@ -250,95 +93,87 @@ ) from .ics import ( - _InitialCondition, + InitialCondition, InitialTemperatureField, InitialStressField, ) -__all__ = [ - 'Model', - - 'DeformablePart', - 'RigidPart', - 'Node', - - '_Element', - 'MassElement', - 'BeamElement', - 'SpringElement', - 'TrussElement', - 'StrutElement', - 'TieElement', - 'ShellElement', - 'MembraneElement', - '_Element3D', - 'TetrahedronElement', - 'HexahedronElement', - - '_Material', - 'UserMaterial', - 'Concrete', - 'ConcreteSmearedCrack', - 'ConcreteDamagedPlasticity', - 'ElasticIsotropic', - 'Stiff', - 'ElasticOrthotropic', - 'ElasticPlastic', - 'Steel', - 'Timber', - 'HardContactFrictionPenalty', - 'HardContactNoFriction', - 'HardContactRough', - - '_Section', - 'MassSection', - 'BeamSection', - 'SpringSection', - 'AngleSection', - 'BoxSection', - 'CircularSection', - 'HexSection', - 'ISection', - 'PipeSection', - 'RectangularSection', - 'ShellSection', - 'MembraneSection', - 'SolidSection', - 'TrapezoidalSection', - 'TrussSection', - 'StrutSection', - 'TieSection', - - '_Constraint', - '_MultiPointConstraint', - 'TieMPC', - 'BeamMPC', - 'TieConstraint', - - '_BeamEndRelease', - 'BeamEndPinRelease', - - '_Group', - 'NodesGroup', - 'ElementsGroup', - 'FacesGroup', - 'PartsGroup', - - '_BoundaryCondition', - 'GeneralBC', - 'FixedBC', - 'PinnedBC', - 'ClampBCXX', - 'ClampBCYY', - 'ClampBCZZ', - 'RollerBCX', - 'RollerBCY', - 'RollerBCZ', - 'RollerBCXY', - 'RollerBCYZ', - 'RollerBCXZ', - - '_InitialCondition', - 'InitialTemperatureField', - 'InitialStressField', +__all__ = [ + "Model", + "DeformablePart", + "RigidPart", + "Node", + "Element", + "MassElement", + "BeamElement", + "SpringElement", + "TrussElement", + "StrutElement", + "TieElement", + "ShellElement", + "MembraneElement", + "Element3D", + "TetrahedronElement", + "HexahedronElement", + "Material", + "UserMaterial", + "Concrete", + "ConcreteSmearedCrack", + "ConcreteDamagedPlasticity", + "ElasticIsotropic", + "Stiff", + "ElasticOrthotropic", + "ElasticPlastic", + "Steel", + "Timber", + "HardContactFrictionPenalty", + "HardContactNoFriction", + "HardContactRough", + "Section", + "MassSection", + "BeamSection", + "SpringSection", + "AngleSection", + "BoxSection", + "CircularSection", + "HexSection", + "ISection", + "PipeSection", + "RectangularSection", + "ShellSection", + "MembraneSection", + "SolidSection", + "TrapezoidalSection", + "TrussSection", + "StrutSection", + "TieSection", + "Constraint", + "MultiPointConstraint", + "TieMPC", + "BeamMPC", + "TieConstraint", + "BeamEndRelease", + "BeamEndPinRelease", + "BeamEndSliderRelease", + "Group", + "NodesGroup", + "ElementsGroup", + "FacesGroup", + "PartsGroup", + "BoundaryCondition", + "GeneralBC", + "FixedBC", + "PinnedBC", + "ClampBCXX", + "ClampBCYY", + "ClampBCZZ", + "RollerBCX", + "RollerBCY", + "RollerBCZ", + "RollerBCXY", + "RollerBCYZ", + "RollerBCXZ", + "InitialCondition", + "InitialTemperatureField", + "InitialStressField", ] diff --git a/src/compas_fea2/model/bcs.py b/src/compas_fea2/model/bcs.py index 4a60fcb6c..410633ad3 100644 --- a/src/compas_fea2/model/bcs.py +++ b/src/compas_fea2/model/bcs.py @@ -4,53 +4,46 @@ from compas_fea2.base import FEAData -docs = """ -Note ----- -BoundaryConditions are registered to a :class:`compas_fea2.model.Model`. - -Warning -------- -The `axes` parameter is WIP. Currently only global axes can be used. - -Parameters ----------- -name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. -axes : str, optional - The refernce axes. - -Attributes ----------- -name : str - Uniqe identifier. -x : bool - Restrain translations along the x axis. -y : bool - Restrain translations along the y axis. -z : bool - Restrain translations along the z axis. -xx : bool - Restrain rotations around the x axis. -yy : bool - Restrain rotations around the y axis. -zz : bool - Restrain rotations around the z axis. -components : dict - Dictionary with component-value pairs summarizing the boundary condition. -axes : str - The refernce axes. -""" - - -class _BoundaryCondition(FEAData): + +class BoundaryCondition(FEAData): """Base class for all zero-valued boundary conditions. + + Parameters + ---------- + axes : str, optional + The refernce axes. + + Attributes + ---------- + x : bool + Restrain translations along the x axis. + y : bool + Restrain translations along the y axis. + z : bool + Restrain translations along the z axis. + xx : bool + Restrain rotations around the x axis. + yy : bool + Restrain rotations around the y axis. + zz : bool + Restrain rotations around the z axis. + components : dict + Dictionary with component-value pairs summarizing the boundary condition. + axes : str + The reference axes. + + Notes + ----- + BoundaryConditions are registered to a :class:`compas_fea2.model.Model`. + + Warnings + -------- + The `axes` parameter is WIP. Currently only global axes can be used. + """ - __doc__ += docs - def __init__(self, axes='global', name=None, **kwargs): - super(_BoundaryCondition, self).__init__(name=name, **kwargs) + def __init__(self, axes="global", **kwargs): + super(BoundaryCondition, self).__init__(**kwargs) self._axes = axes self._x = False self._y = False @@ -93,32 +86,31 @@ def axes(self, value): @property def components(self): - return {c: getattr(self, c) for c in ['x', 'y', 'z', 'xx', 'yy', 'zz']} - + return {c: getattr(self, c) for c in ["x", "y", "z", "xx", "yy", "zz"]} + + +class GeneralBC(BoundaryCondition): + """Customized boundary condition. + + Parameters + ---------- + x : bool + Restrain translations along the x axis. + y : bool + Restrain translations along the y axis. + z : bool + Restrain translations along the z axis. + xx : bool + Restrain rotations around the x axis. + yy : bool + Restrain rotations around the y axis. + zz : bool + Restrain rotations around the z axis. -class GeneralBC(_BoundaryCondition): - """Costumized boundary condition. - """ - __doc__ += docs - __doc__ += """ -Additional Parameters ---------------------- -x : bool - Restrain translations along the x axis. -y : bool - Restrain translations along the y axis. -z : bool - Restrain translations along the z axis. -xx : bool - Restrain rotations around the x axis. -yy : bool - Restrain rotations around the y axis. -zz : bool - Restrain rotations around the z axis. """ - def __init__(self, name=None, x=False, y=False, z=False, xx=False, yy=False, zz=False, **kwargs): - super(GeneralBC, self).__init__(name=name, **kwargs) + def __init__(self, x=False, y=False, z=False, xx=False, yy=False, zz=False, **kwargs): + super(GeneralBC, self).__init__(**kwargs) self._x = x self._y = y self._z = z @@ -127,13 +119,11 @@ def __init__(self, name=None, x=False, y=False, z=False, xx=False, yy=False, zz= self._zz = zz -class FixedBC(_BoundaryCondition): - """A fixed nodal displacement boundary condition. - """ - __doc__ += docs +class FixedBC(BoundaryCondition): + """A fixed nodal displacement boundary condition.""" - def __init__(self, name=None, **kwargs): - super(FixedBC, self).__init__(name=name, **kwargs) + def __init__(self, **kwargs): + super(FixedBC, self).__init__(**kwargs) self._x = True self._y = True self._z = True @@ -142,104 +132,84 @@ def __init__(self, name=None, **kwargs): self._zz = True -class PinnedBC(_BoundaryCondition): - """A pinned nodal displacement boundary condition. - """ - __doc__ += docs +class PinnedBC(BoundaryCondition): + """A pinned nodal displacement boundary condition.""" - def __init__(self, name=None, **kwargs): - super(PinnedBC, self).__init__(name=name, **kwargs) + def __init__(self, **kwargs): + super(PinnedBC, self).__init__(**kwargs) self._x = True self._y = True self._z = True class ClampBCXX(PinnedBC): - """A pinned nodal displacement boundary condition clamped in XX. - """ - __doc__ += docs + """A pinned nodal displacement boundary condition clamped in XX.""" - def __init__(self, name=None, **kwargs): - super(ClampBCXX, self).__init__(name=name, **kwargs) + def __init__(self, **kwargs): + super(ClampBCXX, self).__init__(**kwargs) self._xx = True class ClampBCYY(PinnedBC): - """A pinned nodal displacement boundary condition clamped in YY. - """ - __doc__ += docs + """A pinned nodal displacement boundary condition clamped in YY.""" - def __init__(self, name=None, **kwargs): - super(ClampBCYY, self).__init__(name=name, **kwargs) + def __init__(self, **kwargs): + super(ClampBCYY, self).__init__(**kwargs) self._yy = True class ClampBCZZ(PinnedBC): - """A pinned nodal displacement boundary condition clamped in ZZ. - """ - __doc__ += docs + """A pinned nodal displacement boundary condition clamped in ZZ.""" - def __init__(self, name=None, **kwargs): - super(ClampBCZZ, self).__init__(name=name, **kwargs) + def __init__(self, **kwargs): + super(ClampBCZZ, self).__init__(**kwargs) self._zz = True class RollerBCX(PinnedBC): - """A pinned nodal displacement boundary condition released in X. - """ - __doc__ += docs + """A pinned nodal displacement boundary condition released in X.""" - def __init__(self, name=None, **kwargs): - super(RollerBCX, self).__init__(name=name, **kwargs) + def __init__(self, **kwargs): + super(RollerBCX, self).__init__(**kwargs) self._x = False class RollerBCY(PinnedBC): - """A pinned nodal displacement boundary condition released in Y. - """ - __doc__ += docs + """A pinned nodal displacement boundary condition released in Y.""" - def __init__(self, name=None, **kwargs): - super(RollerBCY, self).__init__(name=name, **kwargs) + def __init__(self, **kwargs): + super(RollerBCY, self).__init__(**kwargs) self._y = False class RollerBCZ(PinnedBC): - """A pinned nodal displacement boundary condition released in Z. - """ - __doc__ += docs + """A pinned nodal displacement boundary condition released in Z.""" - def __init__(self, name=None, **kwargs): - super(RollerBCZ, self).__init__(name=name, **kwargs) + def __init__(self, **kwargs): + super(RollerBCZ, self).__init__(**kwargs) self._z = False class RollerBCXY(PinnedBC): - """A pinned nodal displacement boundary condition released in X and Y. - """ - __doc__ += docs + """A pinned nodal displacement boundary condition released in X and Y.""" - def __init__(self, name=None, **kwargs): - super(RollerBCXY, self).__init__(name=name, **kwargs) + def __init__(self, **kwargs): + super(RollerBCXY, self).__init__(**kwargs) self._x = False self._y = False class RollerBCYZ(PinnedBC): - """A pinned nodal displacement boundary condition released in Y and Z. - """ - __doc__ += docs + """A pinned nodal displacement boundary condition released in Y and Z.""" - def __init__(self, name=None, **kwargs): - super(RollerBCYZ, self).__init__(name=name, **kwargs) + def __init__(self, **kwargs): + super(RollerBCYZ, self).__init__(**kwargs) self._y = False self._z = False class RollerBCXZ(PinnedBC): - """A pinned nodal displacement boundary condition released in X and Z. - """ - __doc__ += docs + """A pinned nodal displacement boundary condition released in X and Z.""" def __init__(self, name=None, **kwargs): super(RollerBCXZ, self).__init__(name=name, **kwargs) diff --git a/src/compas_fea2/model/connectors.py b/src/compas_fea2/model/connectors.py index c96afa74e..6d0e1b688 100644 --- a/src/compas_fea2/model/connectors.py +++ b/src/compas_fea2/model/connectors.py @@ -5,35 +5,23 @@ from compas_fea2.base import FEAData -class _Connector(FEAData): - """Initialises base Connector object. A Connector links a node to one or more - other nodes in the model. +class Connector(FEAData): + """Base class for connectors. - Note - ---- - Connectors are registered to a :class:`compas_fea2.model.Model`. - - Parameters - ---------- - name : str,optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. + A Connector links a node to one or more other nodes in the model. - Attributes - ---------- - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. + Notes + ----- + Connectors are registered to a :class:`compas_fea2.model.Model`. """ - def __init__(self, *, name=None, **kwargs): - super(_Connector, self).__init__(name, **kwargs) + def __init__(self, **kwargs): + super(Connector, self).__init__(**kwargs) -class Spring(_Connector): - """Elastic spring connector. - """ - __doc__ += _Connector.__doc__ - def __init__(self, name=None, **kwargs): - super(Spring, self).__init__(name=name, **kwargs) +class Spring(Connector): + """Elastic spring connector.""" + + def __init__(self, **kwargs): + super(Spring, self).__init__(**kwargs) diff --git a/src/compas_fea2/model/constraints.py b/src/compas_fea2/model/constraints.py index 3d0a146ea..73a4f8315 100644 --- a/src/compas_fea2/model/constraints.py +++ b/src/compas_fea2/model/constraints.py @@ -5,42 +5,24 @@ from compas_fea2.base import FEAData -class _Constraint(FEAData): - """A constraint removes degree of freedom of nodes in the model. +class Constraint(FEAData): + """Base class for constraints. - Note - ---- - Constraints are registered to a :class:`compas_fea2.model.Model`. - - Parameters - ---------- - name : str,optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. - - Attributes - ---------- - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. + A constraint removes degree of freedom of nodes in the model. """ - def __init__(self, *, name=None, **kwargs): - super(_Constraint, self).__init__(name, **kwargs) + def __init__(self, **kwargs): + super(Constraint, self).__init__(**kwargs) + # ------------------------------------------------------------------------------ # MPC # ------------------------------------------------------------------------------ -class _MultiPointConstraint(_Constraint): - """A MultiPointContrstaint (MPC) links a node (master) to other nodes - (slaves) in the model. - - Note - ---- - Constraints are registered to a :class:`compas_fea2.model.Model`. +class MultiPointConstraint(Constraint): + """A MultiPointContrstaint (MPC) links a node (master) to other nodes (slaves) in the model. Parameters ---------- @@ -50,47 +32,38 @@ class _MultiPointConstraint(_Constraint): List or Group of nodes that act as slaves. tol : float Constraint tolerance, distance limit between master and slaves. - name : str,optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. Attributes ---------- - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. master : :class:`compas_fea2.model.Node` Node that act as master. slaves : [:class:`compas_fea2.model.Node`] | :class:`compas_fea2.model.NodesGroup` List or Group of nodes that act as slaves. tol : float Constraint tolerance, distance limit between master and slaves. + + Notes + ----- + Constraints are registered to a :class:`compas_fea2.model.Model`. + """ - def __init__(self, constraint_type, name=None, **kwargs): - super(_MultiPointConstraint, self).__init__(name=name, **kwargs) + def __init__(self, constraint_type, **kwargs): + super(MultiPointConstraint, self).__init__(**kwargs) self.constraint_type = constraint_type -class TieMPC(_MultiPointConstraint): - """Tie MPC that constraints axial translations. - """ - __doc__ += _MultiPointConstraint.__doc__ +class TieMPC(MultiPointConstraint): + """Tie MPC that constraints axial translations.""" -class BeamMPC(_MultiPointConstraint): - """Beam MPC that constraints axial translations and rotations. - """ - __doc__ += _MultiPointConstraint.__doc__ +class BeamMPC(MultiPointConstraint): + """Beam MPC that constraints axial translations and rotations.""" -#TODO check! -class _SurfaceConstraint(_Constraint): - """A SurfaceContrstaint links a surface (master) to another surface (slave) - in the model. - Note - ---- - Constraints are registered to a :class:`compas_fea2.model.Model`. +# TODO check! +class SurfaceConstraint(Constraint): + """A SurfaceContrstaint links a surface (master) to another surface (slave) in the model. Parameters ---------- @@ -100,23 +73,18 @@ class _SurfaceConstraint(_Constraint): List or Group of nodes that act as slaves. tol : float Constraint tolerance, distance limit between master and slaves. - name : str,optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. Attributes ---------- - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. master : :class:`compas_fea2.model.Node` Node that act as master. slaves : [:class:`compas_fea2.model.Node`] | :class:`compas_fea2.model.NodesGroup` List or Group of nodes that act as slaves. tol : float Constraint tolerance, distance limit between master and slaves. - """ -class TieConstraint(_SurfaceConstraint): - """Tie constraint between two surfaces. """ + + +class TieConstraint(SurfaceConstraint): + """Tie constraint between two surfaces.""" diff --git a/src/compas_fea2/model/elements.py b/src/compas_fea2/model/elements.py index 240ec87ea..7e57d45cd 100644 --- a/src/compas_fea2/model/elements.py +++ b/src/compas_fea2/model/elements.py @@ -7,34 +7,16 @@ from compas.geometry import Frame from compas.geometry import Plane from compas.utilities import pairwise -from compas.datastructures import Mesh from compas.geometry import Polygon -from compas.datastructures import mesh_thicken -import compas_fea2 from compas_fea2.base import FEAData - -class _Element(FEAData): +class Element(FEAData): """Initialises a base Element object. - Note - ---- - Elements are registered to the same :class:`compas_fea2.model._Part` as well - as its nodes and can belong to only one Part. - - Warning - ------- - When an Element is added to a Part, the nodes of the elements are also added - and registered to the same part. This might change the original registration - of the nodes! - Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. nodes : list[:class:`compas_fea2.model.Node`] Ordered list of node identifiers to which the element connects. section : :class:`compas_fea2.model._Section` @@ -47,8 +29,6 @@ class _Element(FEAData): Attributes ---------- - name : str - Uniqe identifier. key : int, read-only Identifier of the element in the parent part. nodes : list[:class:`compas_fea2.model.Node`] @@ -78,11 +58,23 @@ class _Element(FEAData): rigid : bool, read-only Define the element as rigid (no deformations allowed) or not. For Rigid elements sections are not needed. + + Notes + ----- + Elements and their nodes are registered to the same :class:`compas_fea2.model._Part` and can belong to only one Part. + + Warnings + -------- + When an Element is added to a Part, the nodes of the elements are also added + and registered to the same part. This might change the original registration + of the nodes! + """ -# FIXME frame and orientations are a bit different concepts. find a way to unify them - def __init__(self, *, nodes, section, frame=None, implementation=None, name=None, **kwargs): - super(_Element, self).__init__(name, **kwargs) + # FIXME frame and orientations are a bit different concepts. find a way to unify them + + def __init__(self, nodes, section, frame=None, implementation=None, **kwargs): + super(Element, self).__init__(**kwargs) self._nodes = self._check_nodes(nodes) self._registration = nodes[0]._registration self._section = section @@ -90,10 +82,8 @@ def __init__(self, *, nodes, section, frame=None, implementation=None, name=None self._implementation = implementation self._on_boundary = None self._key = None - self._area = None self._volume = None - self._results = {} self._rigid = False @@ -119,7 +109,7 @@ def nodes(self, value): @property def nodes_key(self): - return '-'.join(sorted([str(node.key) for node in self.nodes], key=int)) + return "-".join(sorted([str(node.key) for node in self.nodes], key=int)) @property def section(self): @@ -153,7 +143,7 @@ def on_boundary(self, value): def _check_nodes(self, nodes): if len(set([node._registration for node in nodes])) != 1: - raise ValueError('At least one of node is registered to a different part or not registered') + raise ValueError("At least one of node is registered to a different part or not registered") return nodes @property @@ -172,25 +162,26 @@ def results(self): def rigid(self): return self._rigid + # ============================================================================== # 0D elements # ============================================================================== -class MassElement(_Element): - """A 0D element for concentrated point mass. - """ +class MassElement(Element): + """A 0D element for concentrated point mass.""" # ============================================================================== # 1D elements # ============================================================================== -class _Element1D(_Element): - """Element with 1 dimension. - """ -class BeamElement(_Element1D): +class Element1D(Element): + """Element with 1 dimension.""" + + +class BeamElement(Element1D): """A 1D element that resists axial, shear, bending and torsion. A beam element is a one-dimensional line element in three-dimensional space @@ -201,40 +192,60 @@ class BeamElement(_Element1D): """ -class SpringElement(_Element1D): - """A 1D spring element. - """ +class SpringElement(Element1D): + """A 1D spring element.""" -class TrussElement(_Element1D): - """A 1D element that resists axial loads. - """ +class TrussElement(Element1D): + """A 1D element that resists axial loads.""" class StrutElement(TrussElement): - """A truss element that resists axial compressive loads. - """ + """A truss element that resists axial compressive loads.""" class TieElement(TrussElement): - """A truss element that resists axial tensile loads. - """ + """A truss element that resists axial tensile loads.""" # ============================================================================== # 2D elements # ============================================================================== + class Face(FEAData): - """_summary_ + """Element representing a face. Parameters ---------- - FEAData : _type_ - _description_ + nodes : list[:class:`compas_fea2.model.Node`] + Ordered list of node identifiers to which the element connects. + tag : str + The tag of the face. + element : :class:`compas_fea2.model.Element` + The element to which the face belongs. + + Attributes + ---------- + nodes : list[:class:`compas_fea2.model.Node`] + Nodes to which the element is connected. + tag : str + The tag of the face. + element : :class:`compas_fea2.model.Element` + The element to which the face belongs. + plane : :class:`compas.geometry.Plane` + The plane of the face. + polygon : :class:`compas.geometry.Polygon` + The polygon of the face. + area : float + The area of the face. + results : dict + Dictionary with results of the face. + """ - def __init__(self, *, nodes, tag, element=None, name=None): - super(Face, self).__init__(name) + + def __init__(self, nodes, tag, element=None, **kwargs): + super(Face, self).__init__(**kwargs) self._nodes = nodes self._tag = tag self._plane = Plane.from_three_points(*[node.xyz for node in nodes[:3]]) # TODO check when more than 3 nodes @@ -267,26 +278,30 @@ def polygon(self): @property def area(self): - """The area property.""" return self.polygon.area -class _Element2D(_Element): + +class Element2D(Element): """Element with 2 dimensions. - """ - __doc__ += _Element.__doc__ - __doc__ +=""" - Additional Parameters - --------------------- + + Parameters + ---------- faces : [:class:`compas_fea2.model.elements.Face] The faces of the element. faces : dict Dictionary providing for each face the node indices. For example: {'s1': (0,1,2), ...} + """ - def __init__(self, *, nodes, frame, section=None, implementation=None, rigid=False, name=None, **kwargs): - super(_Element2D, self).__init__(nodes=nodes, section=section, - frame=frame, implementation=implementation, name=name, **kwargs) + def __init__(self, nodes, frame, section=None, implementation=None, rigid=False, **kwargs): + super(Element2D, self).__init__( + nodes=nodes, + section=section, + frame=frame, + implementation=implementation, + **kwargs, + ) self._faces = None self._face_indices = None @@ -312,8 +327,7 @@ def faces(self): @property def volume(self): - return self._faces[0].area*self.section.t - + return self._faces[0].area * self.section.t def _construct_faces(self, face_indices): """Construct the face-nodes dictionary. @@ -329,10 +343,13 @@ def _construct_faces(self, face_indices): dict Dictionary with face names and the corresponding nodes. """ - return [Face(nodes=itemgetter(*indices)(self.nodes), tag=name, element=self) for name, indices in face_indices.items()] + return [ + Face(nodes=itemgetter(*indices)(self.nodes), tag=name, element=self) + for name, indices in face_indices.items() + ] -class ShellElement(_Element2D): +class ShellElement(Element2D): """A 2D element that resists axial, shear, bending and torsion. Shell elements are used to model structures in which one dimension, the @@ -340,22 +357,25 @@ class ShellElement(_Element2D): """ - def __init__(self, *, nodes, frame=None, section=None, implementation=None, rigid=False, name=None, **kwargs): - super(ShellElement, self).__init__(nodes=nodes, frame=frame, section=section, - implementation=implementation, rigid=rigid, name=name, **kwargs) - - self._face_indices = { - 'SPOS': tuple(range(len(nodes))), - 'SNEG': tuple(range(len(nodes)))[::-1] - } + def __init__(self, nodes, frame=None, section=None, implementation=None, rigid=False, **kwargs): + super(ShellElement, self).__init__( + nodes=nodes, + frame=frame, + section=section, + implementation=implementation, + rigid=rigid, + **kwargs, + ) + + self._face_indices = {"SPOS": tuple(range(len(nodes))), "SNEG": tuple(range(len(nodes)))[::-1]} self._faces = self._construct_faces(self._face_indices) -class MembraneElement(_Element2D): +class MembraneElement(Element2D): """A shell element that resists only axial loads. - Note - ---- + Notes + ----- Membrane elements are used to represent thin surfaces in space that offer strength in the plane of the element but have no bending stiffness; for example, the thin rubber sheet that forms a balloon. In addition, they are @@ -373,7 +393,7 @@ class MembraneElement(_Element2D): # TODO add picture with node lables convention -class _Element3D(_Element): +class Element3D(Element): """A 3D element that resists axial, shear, bending and torsion. Solid (continuum) elements can be used for linear analysis and for complex nonlinear analyses involving contact, plasticity, and large @@ -384,13 +404,17 @@ class _Element3D(_Element): """ - def __init__(self, *, nodes, section, implementation=None, name=None, **kwargs): - super(_Element3D, self).__init__(nodes=nodes, section=section, frame=None, - implementation=implementation, name=name, **kwargs) + def __init__(self, nodes, section, implementation=None, **kwargs): + super(Element3D, self).__init__( + nodes=nodes, + section=section, + frame=None, + implementation=implementation, + **kwargs, + ) self._face_indices = None self._faces = None - @property def nodes(self): return self._nodes @@ -421,44 +445,49 @@ def _construct_faces(self, face_indices): ------- dict Dictionary with face names and the corresponding nodes. + """ - return [Face(nodes=itemgetter(*indices)(self.nodes), tag=name, element=self) for name, indices in face_indices.items()] + return [ + Face(nodes=itemgetter(*indices)(self.nodes), tag=name, element=self) + for name, indices in face_indices.items() + ] @property def area(self): return self._area @classmethod - def from_polyhedron(cls, polyhedron, section, implementation=None, name=None, **kwargs): - # m = importlib.import_module('.'.join(cls.__module__.split('.')[:-1])) + def from_polyhedron(cls, polyhedron, section, implementation=None, **kwargs): from compas_fea2.model import Node - element = cls([Node(vertex) for vertex in polyhedron.vertices], section, implementation, name, **kwargs) + + element = cls([Node(vertex) for vertex in polyhedron.vertices], section, implementation, **kwargs) return element -class TetrahedronElement(_Element3D): +class TetrahedronElement(Element3D): """A Solid element with 4 faces. - Note - ---- + Notes + ----- The face labels are as follows: - - S1: (0, 1, 2) - - S2: (0, 1, 3) - - S3: (1, 2, 3) - - S4: (0, 2, 3) + + - S1: (0, 1, 2) + - S2: (0, 1, 3) + - S3: (1, 2, 3) + - S4: (0, 2, 3) where the number is the index of the the node in the nodes list + """ - def __init__(self, *, nodes, section, implementation=None, name=None, **kwargs): - super(TetrahedronElement, self).__init__(nodes=nodes, section=section, - implementation=implementation, name=name, **kwargs) - self._face_indices = { - 's1': (0, 1, 2), - 's2': (0, 1, 3), - 's3': (1, 2, 3), - 's4': (0, 2, 3) - } + def __init__(self, *, nodes, section, implementation=None, **kwargs): + super(TetrahedronElement, self).__init__( + nodes=nodes, + section=section, + implementation=implementation, + **kwargs, + ) + self._face_indices = {"s1": (0, 1, 2), "s2": (0, 1, 3), "s3": (1, 2, 3), "s4": (0, 2, 3)} self._faces = self._construct_faces(self._face_indices) @property @@ -471,46 +500,57 @@ def edges(self): seen.add((v, u)) yield u, v - #TODO use compas funcitons to compute differences and det + # TODO use compas funcitons to compute differences and det @property def volume(self): """The volume property.""" def determinant_3x3(m): - return (m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - - m[1][0] * (m[0][1] * m[2][2] - m[0][2] * m[2][1]) + - m[2][0] * (m[0][1] * m[1][2] - m[0][2] * m[1][1])) - + return ( + m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) + - m[1][0] * (m[0][1] * m[2][2] - m[0][2] * m[2][1]) + + m[2][0] * (m[0][1] * m[1][2] - m[0][2] * m[1][1]) + ) def subtract(a, b): - return (a[0] - b[0], - a[1] - b[1], - a[2] - b[2]) + return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) nodes_coord = [node.xyz for node in self.nodes] - a, b, c ,d = nodes_coord - return abs(determinant_3x3((subtract(a, b), - subtract(b, c), - subtract(c, d), - ))) / 6.0 - -class PentahedronElement(_Element3D): - """A Solid element with 5 faces (extruded triangle). - """ - - -class HexahedronElement(_Element3D): - """A Solid cuboid element with 6 faces (extruded rectangle). - """ - - def __init__(self, *, nodes, section, implementation=None, name=None, **kwargs): - super(HexahedronElement, self).__init__(nodes=nodes, section=section, - implementation=implementation, name=name, **kwargs) - self._faces_indices = {'s1': (0, 1, 2, 3), - 's2': (4, 5, 6, 7), - 's3': (0, 1, 4, 5), - 's4': (1, 2, 5, 6), - 's5': (2, 3, 6, 7), - 's6': (0, 3, 4, 7) - } + a, b, c, d = nodes_coord + return ( + abs( + determinant_3x3( + ( + subtract(a, b), + subtract(b, c), + subtract(c, d), + ) + ) + ) + / 6.0 + ) + + +class PentahedronElement(Element3D): + """A Solid element with 5 faces (extruded triangle).""" + + +class HexahedronElement(Element3D): + """A Solid cuboid element with 6 faces (extruded rectangle).""" + + def __init__(self, nodes, section, implementation=None, **kwargs): + super(HexahedronElement, self).__init__( + nodes=nodes, + section=section, + implementation=implementation, + **kwargs, + ) + self._faces_indices = { + "s1": (0, 1, 2, 3), + "s2": (4, 5, 6, 7), + "s3": (0, 1, 4, 5), + "s4": (1, 2, 5, 6), + "s5": (2, 3, 6, 7), + "s6": (0, 3, 4, 7), + } self._faces = self._construct_faces(self._face_indices) diff --git a/src/compas_fea2/model/groups.py b/src/compas_fea2/model/groups.py index 51f1f2a3f..e115c2af8 100644 --- a/src/compas_fea2/model/groups.py +++ b/src/compas_fea2/model/groups.py @@ -8,7 +8,7 @@ # TODO change lists to sets -class _Group(FEAData): +class Group(FEAData): """Base class for all groups. Parameters @@ -16,20 +16,16 @@ class _Group(FEAData): members : set, optional Set with the members belonging to the group. These can be either node, elements, faces or parts. By default ``None``. - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. Attributes ---------- registration : :class:`compas_fea2.model.DeformablePart` | :class:`compas_fea2.model.Model` The parent object where the members of the Group belong. - name : str - Uniqe identifier. + """ - def __init__(self, members=None, name=None, **kwargs): - super(_Group, self).__init__(name=name, **kwargs) + def __init__(self, members=None, **kwargs): + super(Group, self).__init__(**kwargs) self._members = set() if not members else self._check_members(members) def __str__(self): @@ -38,39 +34,42 @@ def __str__(self): {} name : {} # of members : {} -""".format(self.__class__.__name__, - len(self.__class__.__name__) * '-', - self.name, - len(self._members)) +""".format( + self.__class__.__name__, len(self.__class__.__name__) * "-", self.name, len(self._members) + ) def _check_member(self, member): if not isinstance(self, FacesGroup): if member._registration != self._registration: - raise ValueError('{} is registered to a different object'.format(member)) + raise ValueError("{} is registered to a different object".format(member)) return member def _check_members(self, members): if not members or not isinstance(members, Iterable): - raise ValueError('{} must be a not empty iterable'.format(members)) + raise ValueError("{} must be a not empty iterable".format(members)) # FIXME combine in more pythonic way if isinstance(self, FacesGroup): if len(set([member.element._registration for member in members])) != 1: raise ValueError( - 'At least one of the members to add is registered to a different object or not registered') + "At least one of the members to add is registered to a different object or not registered" + ) if self._registration: if list(members).pop().element._registration != self._registration: raise ValueError( - 'At least one of the members to add is registered to a different object than the group') + "At least one of the members to add is registered to a different object than the group" + ) else: self._registration = list(members).pop().element._registration else: if len(set([member._registration for member in members])) != 1: raise ValueError( - 'At least one of the members to add is registered to a different object or not registered') + "At least one of the members to add is registered to a different object or not registered" + ) if self._registration: if list(members).pop()._registration != self._registration: raise ValueError( - 'At least one of the members to add is registered to a different object than the group') + "At least one of the members to add is registered to a different object than the group" + ) else: self._registration = list(members).pop()._registration return members @@ -110,36 +109,32 @@ def _add_members(self, members): return members -class NodesGroup(_Group): +class NodesGroup(Group): """Base class nodes groups. - Note - ---- - NodesGroups are registered to the same :class:`compas_fea2.model._Part` as its nodes - and can belong to only one Part. - Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. nodes : list[:class:`compas_fea2.model.Node`] The nodes belonging to the group. Attributes ---------- - name : str - Uniqe identifier. nodes : list[:class:`compas_fea2.model.Node`] The nodes belonging to the group. part : :class:`compas_fea2.model._Part` The part where the group is registered, by default `None`. model : :class:`compas_fea2.model.Model` The model where the group is registered, by default `None`. + + Notes + ----- + NodesGroups are registered to the same :class:`compas_fea2.model._Part` as its nodes + and can belong to only one Part. + """ - def __init__(self, *, nodes, name=None, **kwargs): - super(NodesGroup, self).__init__(members=nodes, name=name, **kwargs) + def __init__(self, nodes, **kwargs): + super(NodesGroup, self).__init__(members=nodes, **kwargs) @property def part(self): @@ -184,37 +179,32 @@ def add_nodes(self, nodes): return self._add_members(nodes) -class ElementsGroup(_Group): +class ElementsGroup(Group): """Base class for elements groups. - Note - ---- - ElementsGroups are registered to the same :class:`compas_fea2.model.DeformablePart` as - its elements and can belong to only one DeformablePart. - Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. elements : list[:class:`compas_fea2.model.Element`] The elements belonging to the group. Attributes ---------- - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. elements : list[:class:`compas_fea2.model.Element`] The elements belonging to the group. part : :class:`compas_fea2.model._Part` The part where the group is registered, by default `None`. model : :class:`compas_fea2.model.Model` The model where the group is registered, by default `None`. + + Notes + ----- + ElementsGroups are registered to the same :class:`compas_fea2.model.DeformablePart` as + its elements and can belong to only one DeformablePart. + """ - def __init__(self, *, elements, name=None, **kwargs): - super(ElementsGroup, self).__init__(members=elements, name=name, **kwargs) + def __init__(self, elements, **kwargs): + super(ElementsGroup, self).__init__(members=elements, **kwargs) @property def part(self): @@ -233,13 +223,14 @@ def add_element(self, element): Parameters ---------- - element : :class:`compas_fea2.model._Element` + element : :class:`compas_fea2.model.Element` The element to add. Returns ------- - :class:`compas_fea2.model._Element` + :class:`compas_fea2.model.Element` The element added. + """ return self._add_member(element) @@ -248,25 +239,21 @@ def add_elements(self, elements): Parameters ---------- - elements : [:class:`compas_fea2.model._Element`] + elements : [:class:`compas_fea2.model.Element`] The elements to add. Returns ------- - [:class:`compas_fea2.model._Element`] + [:class:`compas_fea2.model.Element`] The elements added. + """ return self._add_members(elements) -class FacesGroup(_Group): +class FacesGroup(Group): """Base class elements faces groups. - Note - ---- - FacesGroups are registered to the same :class:`compas_fea2.model.DeformablePart` as the - elements of its faces. - Parameters ---------- name : str, optional @@ -288,6 +275,12 @@ class FacesGroup(_Group): The part where the group is registered, by default `None`. model : :class:`compas_fea2.model.Model` The model where the group is registered, by default `None`. + + Notes + ----- + FacesGroups are registered to the same :class:`compas_fea2.model.DeformablePart` as the + elements of its faces. + """ def __init__(self, *, faces, name=None, **kwargs): @@ -325,6 +318,7 @@ def add_face(self, face): ------- :class:`compas_fea2.model.Face` The element added. + """ return self._add_member(face) @@ -340,16 +334,13 @@ def add_faces(self, faces): ------- [:class:`compas_fea2.model.Face`] The faces added. + """ return self._add_members(faces) -class PartsGroup(_Group): - """Base class for parts groups. - Note - ---- - PartsGroups are registered to the same :class:`compas_fea2.model.Model` as its - parts and can belong to only one Model. +class PartsGroup(Group): + """Base class for parts groups. Parameters ---------- @@ -365,10 +356,16 @@ class PartsGroup(_Group): The parts belonging to the group. model : :class:`compas_fea2.model.Model` The model where the group is registered, by default `None`. + + Notes + ----- + PartsGroups are registered to the same :class:`compas_fea2.model.Model` as its + parts and can belong to only one Model. + """ def __init__(self, *, parts, name=None, **kwargs): - super(PartsGroup, self).__init__(members=parts, name=name, **kwargs) + super(PartsGroup, self).__init__(members=parts, name=name, **kwargs) @property def model(self): diff --git a/src/compas_fea2/model/ics.py b/src/compas_fea2/model/ics.py index 1754b5edb..03202e8d9 100644 --- a/src/compas_fea2/model/ics.py +++ b/src/compas_fea2/model/ics.py @@ -5,56 +5,43 @@ from compas_fea2.base import FEAData -class _InitialCondition(FEAData): +class InitialCondition(FEAData): """Base class for all predefined initial conditions. - Note - ---- + Notes + ----- InitialConditions are registered to a :class:`compas_fea2.model.Model`. The same InitialCondition can be assigned to Nodes or Elements in multiple Parts - Parameters - ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. - - Attributes - ---------- - name : str - Uniqe identifier. """ - def __init__(self, name=None, **kwargs): - super(_InitialCondition, self).__init__(name=name, **kwargs) + def __init__(self, **kwargs): + super(InitialCondition, self).__init__(**kwargs) -#FIXME this is not really a field in the sense that it is only applied to 1 node/element -class InitialTemperatureField(_InitialCondition): - """Temperature field. - Note - ---- - InitialConditions are registered to a :class:`compas_fea2.model.Model`. The - same InitialCondition can be assigned to Nodes or Elements in multiple Parts +# FIXME this is not really a field in the sense that it is only applied to 1 node/element +class InitialTemperatureField(InitialCondition): + """Temperature field. Parameters ---------- temperature : float The temperature value. - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. Attributes ---------- temperature : float The temperature value. - name : str - Uniqe identifier. + + Notes + ----- + InitialConditions are registered to a :class:`compas_fea2.model.Model`. The + same InitialCondition can be assigned to Nodes or Elements in multiple Parts + """ - def __init__(self, temperature, name=None, **kwargs): - super(InitialTemperatureField, self).__init__(name, **kwargs) + def __init__(self, temperature, **kwargs): + super(InitialTemperatureField, self).__init__(**kwargs) self._t = temperature @property @@ -66,32 +53,28 @@ def temperature(self, value): self._t = value -class InitialStressField(_InitialCondition): +class InitialStressField(InitialCondition): """Stress field. - Note - ---- - InitialConditions are registered to a :class:`compas_fea2.model.Model`. The - same InitialCondition can be assigned to Nodes or Elements in multiple Parts - Parameters ---------- stress : touple(float, float, float) The stress values. - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. Attributes ---------- stress : touple(float, float, float) The stress values. - name : str - Uniqe identifier. + + Notes + ----- + InitialConditions are registered to a :class:`compas_fea2.model.Model` + The same InitialCondition can be assigned to Nodes or Elements in multiple Parts. + """ - def __init__(self, stress, name=None, **kwargs): - super(InitialStressField, self).__init__(name, **kwargs) + def __init__(self, stress, **kwargs): + super(InitialStressField, self).__init__(**kwargs) self._s = stress @property diff --git a/src/compas_fea2/model/materials/__init__.py b/src/compas_fea2/model/materials/__init__.py index f4a43d8db..e69de29bb 100644 --- a/src/compas_fea2/model/materials/__init__.py +++ b/src/compas_fea2/model/materials/__init__.py @@ -1,70 +0,0 @@ -""" -******************************************************************************** -Materials -******************************************************************************** - -.. currentmodule:: compas_fea2.model.materials - -General Materials -================= - -.. autosummry:: - :toctree: generated/ - - _Material - ElasticIsotropic - ElasticOrthotropic - ElasticPlastic - Stiff - UserMaterial - -Concrete -======== - -.. autosummary:: - :toctree: generated/ - - Concrete - ConcreteDamagedPlasticity - ConcreteSmearedCrack - -Steel -===== - -.. autosummary:: - :toctree: generated/ - - Steel - -Timber -====== - -.. autosummary:: - :toctree: generated/ - - Timber - -""" - -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -# Concrete -from .concrete import Concrete # noqa : F401 -from .concrete import ConcreteDamagedPlasticity # noqa : F401 -from .concrete import ConcreteSmearedCrack # noqa : F401 - -# Basic Materials -from .material import _Material # noqa : F401 -from .material import ElasticIsotropic # noqa : F401 -from .material import ElasticOrthotropic # noqa : F401 -from .material import ElasticPlastic # noqa : F401 -from .material import Stiff # noqa : F401 -from .material import UserMaterial # noqa : F401 - -# Steel -from .steel import Steel # noqa : F401 - -# Timber -from .timber import Timber diff --git a/src/compas_fea2/model/materials/concrete.py b/src/compas_fea2/model/materials/concrete.py index cd07c24b6..da001b4ad 100644 --- a/src/compas_fea2/model/materials/concrete.py +++ b/src/compas_fea2/model/materials/concrete.py @@ -3,22 +3,11 @@ from __future__ import print_function from math import log -from .material import _Material -from ...utilities._utils import extend_docstring +from .material import Material -@extend_docstring(_Material) -class Concrete(_Material): - """ - Concrete - ======== - Elastic and plastic-cracking Eurocode based concrete material - - Note - ---- - The concrete model is based on Eurocode 2 up to fck=90 MPa. - - Additional Parameters and attributes +class Concrete(Material): + """Elastic and plastic-cracking Eurocode based concrete material Parameters ---------- @@ -45,22 +34,27 @@ class Concrete(_Material): Parameters for modelling the tension side of the stess--strain curve compression : dict Parameters for modelling the tension side of the stess--strain curve + + Notes + ----- + The concrete model is based on Eurocode 2 up to fck=90 MPa. + """ - def __init__(self, *, fck, v=0.2, density=2400, fr=None, name=None, **kwargs): - super(Concrete, self).__init__(density=density, name=name, **kwargs) + def __init__(self, *, fck, v=0.2, density=2400, fr=None, name=None, **kwargs): + super(Concrete, self).__init__(density=density, name=name, **kwargs) de = 0.0001 fcm = fck + 8 - Ecm = 22 * 10**3 * (fcm / 10)**0.3 + Ecm = 22 * 10**3 * (fcm / 10) ** 0.3 ec1 = min(0.7 * fcm**0.31, 2.8) * 0.001 - ecu1 = 0.0035 if fck < 50 else (2.8 + 27 * ((98 - fcm) / 100.)**4) * 0.001 + ecu1 = 0.0035 if fck < 50 else (2.8 + 27 * ((98 - fcm) / 100.0) ** 4) * 0.001 k = 1.05 * Ecm * ec1 / fcm e = [i * de for i in range(int(ecu1 / de) + 1)] ec = [ei - e[1] for ei in e[1:]] - fctm = 0.3 * fck**(2 / 3) if fck <= 50 else 2.12 * log(1 + fcm / 10) - f = [10**6 * fcm * (k * (ei / ec1) - (ei / ec1)**2) / (1 + (k - 2) * (ei / ec1)) for ei in e] + fctm = 0.3 * fck ** (2 / 3) if fck <= 50 else 2.12 * log(1 + fcm / 10) + f = [10**6 * fcm * (k * (ei / ec1) - (ei / ec1) ** 2) / (1 + (k - 2) * (ei / ec1)) for ei in e] E = f[1] / e[1] ft = [1.0, 0.0] @@ -76,8 +70,8 @@ def __init__(self, *, fck, v=0.2, density=2400, fr=None, name=None, **kwargs): self.et = et self.fr = fr # TODO these necessary if we have the above? - self.tension = {'f': ft, 'e': et} - self.compression = {'f': f[1:], 'e': ec} + self.tension = {"f": ft, "e": et} + self.compression = {"f": f[1:], "e": ec} @property def G(self): @@ -95,18 +89,16 @@ def __str__(self): G : {} fck : {} fr : {} -""".format(self.name, self.density, self.E, self.v, self.G, self.fck, self.fr) +""".format( + self.name, self.density, self.E, self.v, self.G, self.fck, self.fr + ) -@extend_docstring(_Material) -class ConcreteSmearedCrack(_Material): - """ - ConcreteSmearedCrack - ==================== - Elastic and plastic, cracking concrete material. +class ConcreteSmearedCrack(Material): + """Elastic and plastic, cracking concrete material. - Additional Parameters and Attributes - ------------------------------------ + Parameters + ---------- E : float Young's modulus E. v : float @@ -122,8 +114,8 @@ class ConcreteSmearedCrack(_Material): fr : list Failure ratios. - Additional Attributes - --------------------- + Attributes + ---------- E : float Young's modulus E. v : float @@ -144,10 +136,11 @@ class ConcreteSmearedCrack(_Material): Parameters for modelling the tension side of the stess--strain curve compression : dict Parameters for modelling the tension side of the stess--strain curve + """ - def __init__(self, *, E, v, density, fc, ec, ft, et, fr=[1.16, 0.0836], name=None, **kwargs): - super(ConcreteSmearedCrack, self).__init__(density=density, name=name, **kwargs) + def __init__(self, *, E, v, density, fc, ec, ft, et, fr=[1.16, 0.0836], **kwargs): + super(ConcreteSmearedCrack, self).__init__(density=density, **kwargs) self.E = E self.v = v @@ -157,8 +150,8 @@ def __init__(self, *, E, v, density, fc, ec, ft, et, fr=[1.16, 0.0836], name=Non self.et = et self.fr = fr # are these necessary if we have the above? - self.tension = {'f': ft, 'e': et} - self.compression = {'f': fc, 'e': ec} + self.tension = {"f": ft, "e": et} + self.compression = {"f": fc, "e": ec} @property def G(self): @@ -179,16 +172,16 @@ def __str__(self): ft : {} et : {} fr : {} -""".format(self.name, self.density, self.E, self.v, self.G, self.fc, self.ec, self.ft, self.et, self.fr) +""".format( + self.name, self.density, self.E, self.v, self.G, self.fc, self.ec, self.ft, self.et, self.fr + ) -class ConcreteDamagedPlasticity(_Material): +class ConcreteDamagedPlasticity(Material): """Damaged plasticity isotropic and homogeneous material. - """ - __doc__ += _Material.__doc__ - __doc__ += """ - Additional Parameters - --------------------- + + Parameters + ---------- E : float Young's modulus E. v : float @@ -200,8 +193,8 @@ class ConcreteDamagedPlasticity(_Material): stiffening : list Tension stiffening parameters. - Additional Attributes - --------------------- + Attributes + ---------- E : float Young's modulus E. v : float @@ -217,8 +210,8 @@ class ConcreteDamagedPlasticity(_Material): """ - def __init__(self, *, E, v, density, damage, hardening, stiffening, name=None, **kwargs): - super(ConcreteDamagedPlasticity, self).__init__(density=density, name=name, **kwargs) + def __init__(self, *, E, v, density, damage, hardening, stiffening, **kwargs): + super(ConcreteDamagedPlasticity, self).__init__(density=density, **kwargs) self.E = E self.v = v diff --git a/src/compas_fea2/model/materials/material.py b/src/compas_fea2/model/materials/material.py index 9ac78b513..74d76aeb2 100644 --- a/src/compas_fea2/model/materials/material.py +++ b/src/compas_fea2/model/materials/material.py @@ -3,19 +3,10 @@ from __future__ import print_function from compas_fea2.base import FEAData -from compas_fea2.utilities._utils import extend_docstring -class _Material(FEAData): - """ - Note - ---- - Materials are registered to a :class:`compas_fea2.model.Model`. The same - material can be assigned to multiple sections and in different elements and - parts. - - Basic Material parameters and attributes - ======================================== +class Material(FEAData): + """Base class for representing materials. Parameters ---------- @@ -23,20 +14,9 @@ class _Material(FEAData): Density of the material. expansion : float, optional Thermal expansion coefficient, by default None. - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. - - Other Parameters - ---------------- - **kwargs : dict - Backend dependent keyword arguments. - See the individual backends for more information. Attributes ---------- - name : str, optional - Uniqe identifier. density : float Density of the material. expansion : float @@ -47,10 +27,16 @@ class _Material(FEAData): model : :class:`compas_fea2.model.Model` The Model where the material is assigned. + Notes + ----- + Materials are registered to a :class:`compas_fea2.model.Model`. The same + material can be assigned to multiple sections and in different elements and + parts. + """ - def __init__(self, *, density, expansion=None, name=None, **kwargs): - super(_Material, self).__init__(name=name, **kwargs) + def __init__(self, *, density, expansion=None, **kwargs): + super(Material, self).__init__(**kwargs) self.density = density self.expansion = expansion self._key = None @@ -70,7 +56,9 @@ def __str__(self): name : {} density : {} expansion : {} -""".format(self.__class__.__name__, len(self.__class__.__name__) * '-', self.name, self.density, self.expansion) +""".format( + self.__class__.__name__, len(self.__class__.__name__) * "-", self.name, self.density, self.expansion + ) def __html__(self): return """ @@ -78,17 +66,14 @@ def __html__(self):

Hello World!

""" + # ============================================================================== # linear elastic # ============================================================================== -@extend_docstring(_Material) -class ElasticOrthotropic(_Material): - """ - ElasticOrthotropic material - =========================== - Elastic, orthotropic and homogeneous material - Additional paramenters and attributes: + +class ElasticOrthotropic(Material): + """Elastic, orthotropic and homogeneous material. Parameters ---------- @@ -162,15 +147,26 @@ def __str__(self): Gxy : {} Gyz : {} Gzx : {} -""".format(self.__class__.__name__, len(self.__class__.__name__) * '-', - self.name, self.density, self.expansion, - self.Ex, self.Ey, self.Ez, self.vxy, self.vyz, self.vzx, self.Gxy, self.Gyz, self.Gzx) - -@extend_docstring(_Material) -class ElasticIsotropic(_Material): - """ - Elastic, isotropic and homogeneous material - =========================================== +""".format( + self.__class__.__name__, + len(self.__class__.__name__) * "-", + self.name, + self.density, + self.expansion, + self.Ex, + self.Ey, + self.Ez, + self.vxy, + self.vyz, + self.vzx, + self.Gxy, + self.Gyz, + self.Gzx, + ) + + +class ElasticIsotropic(Material): + """Elastic, isotropic and homogeneous material Parameters ---------- @@ -187,6 +183,7 @@ class ElasticIsotropic(_Material): Poisson's ratio v. G : float Shear modulus (automatically computed from E and v) + """ def __init__(self, *, E, v, density, expansion=None, name=None, **kwargs): @@ -205,15 +202,18 @@ def __str__(self): E : {} v : {} G : {} -""".format(self.name, self.density, self.expansion, self.E, self.v, self.G) +""".format( + self.name, self.density, self.expansion, self.E, self.v, self.G + ) @property def G(self): return 0.5 * self.E / (1 + self.v) -class Stiff(_Material): - """Elastic, very stiff and massless material. - """ + +class Stiff(Material): + """Elastic, very stiff and massless material.""" + def __init__(self, *, density, expansion=None, name=None, **kwargs): raise NotImplementedError() @@ -221,15 +221,10 @@ def __init__(self, *, density, expansion=None, name=None, **kwargs): # ============================================================================== # non-linear general # ============================================================================== -@extend_docstring(_Material) -class ElasticPlastic(ElasticIsotropic): - """ - ElasticPlastic - ============== - Elastic and plastic, isotropic and homogeneous material. - Additional parameters and attributes. +class ElasticPlastic(ElasticIsotropic): + """Elastic and plastic, isotropic and homogeneous material. Parameters ---------- @@ -271,18 +266,22 @@ def __str__(self): G : {} strain_stress : {} -""".format(self.name, self.density, self.expansion, self.E, self.v, self.G, self.strain_stress) +""".format( + self.name, self.density, self.expansion, self.E, self.v, self.G, self.strain_stress + ) # ============================================================================== # User-defined Materials # ============================================================================== + + class UserMaterial(FEAData): - """ User Defined Material. Tho implement this type of material, a + """User Defined Material. Tho implement this type of material, a separate subroutine is required """ def __init__(self, name=None, **kwargs): super(UserMaterial, self).__init__(self, name=name, **kwargs) - raise NotImplementedError('This class is not available for the selected backend plugin') + raise NotImplementedError("This class is not available for the selected backend plugin") diff --git a/src/compas_fea2/model/materials/steel.py b/src/compas_fea2/model/materials/steel.py index 561e30b48..0a0eeecb5 100644 --- a/src/compas_fea2/model/materials/steel.py +++ b/src/compas_fea2/model/materials/steel.py @@ -3,16 +3,14 @@ from __future__ import print_function from compas_fea2 import units -from .material import _Material, ElasticIsotropic +from .material import ElasticIsotropic class Steel(ElasticIsotropic): """Bi-linear steel with given yield stress. - """ - __doc__ += _Material.__doc__ - __doc__ += """ - Additional Parameters - --------------------- + + Parameters + ---------- E : float Young's modulus E. v : float @@ -43,8 +41,8 @@ class Steel(ElasticIsotropic): """ - def __init__(self, *, fy, fu, eu, E, v, density, name=None, **kwargs): - super(Steel, self).__init__(E=E, v=v, density=density, name=name, **kwargs) + def __init__(self, *, fy, fu, eu, E, v, density, **kwargs): + super(Steel, self).__init__(E=E, v=v, density=density, **kwargs) fu = fu or fy @@ -65,8 +63,8 @@ def __init__(self, *, fy, fu, eu, E, v, density, name=None, **kwargs): self.ep = ep self.E = E self.v = v - self.tension = {'f': f, 'e': e} - self.compression = {'f': fc, 'e': ec} + self.tension = {"f": f, "e": e} + self.compression = {"f": fc, "e": ec} def __str__(self): return """ @@ -82,17 +80,19 @@ def __str__(self): v : {:.2f} eu : {:.2f} ep : {:.2f} -""".format(self.name, - (self.density * units['kg/m**2']), - (self.E * units.pascal).to('GPa'), - (self.G * units.pascal).to('GPa'), - (self.fy * units.pascal).to('MPa'), - (self.fu * units.pascal).to('MPa'), - (self.v * units.dimensionless), - (self.eu * units.dimensionless), - (self.ep * units.dimensionless)) +""".format( + self.name, + (self.density * units["kg/m**2"]), + (self.E * units.pascal).to("GPa"), + (self.G * units.pascal).to("GPa"), + (self.fy * units.pascal).to("MPa"), + (self.fu * units.pascal).to("MPa"), + (self.v * units.dimensionless), + (self.eu * units.dimensionless), + (self.ep * units.dimensionless), + ) - #TODO check values and make unit independent + # TODO check values and make unit independent @classmethod def S355(cls): """Steel S355. diff --git a/src/compas_fea2/model/model.py b/src/compas_fea2/model/model.py index 20c092ea3..3bf4d3abc 100644 --- a/src/compas_fea2/model/model.py +++ b/src/compas_fea2/model/model.py @@ -5,37 +5,30 @@ import importlib import gc import pathlib -from typing import Callable, Iterable, Type -import pint from itertools import groupby -from pathlib import Path, PurePath +from pathlib import Path import os import pickle import compas_fea2 from compas_fea2.utilities._utils import timer -from compas_fea2.utilities._utils import part_method, get_docstring, problem_method, extend_docstring + +# from compas_fea2.utilities._utils import part_method, get_docstring, problem_method from compas_fea2.base import FEAData -from compas_fea2.model.parts import _Part, DeformablePart, RigidPart +from compas_fea2.model.parts import Part, RigidPart from compas_fea2.model.nodes import Node -from compas_fea2.model.elements import _Element -from compas_fea2.model.bcs import _BoundaryCondition -from compas_fea2.model.ics import _InitialCondition, InitialStressField -from compas_fea2.model.groups import _Group, NodesGroup, PartsGroup, ElementsGroup, FacesGroup -from compas_fea2.model.constraints import _Constraint, TieMPC, BeamMPC - +from compas_fea2.model.elements import Element +from compas_fea2.model.bcs import BoundaryCondition +from compas_fea2.model.ics import InitialCondition +from compas_fea2.model.groups import Group, NodesGroup, PartsGroup, ElementsGroup -from compas_fea2.units import units class Model(FEAData): """Class representing an FEA model. Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. description : str, optional Some description of the model, by default ``None``. This will be added to the input file and can be useful for future reference. @@ -45,9 +38,6 @@ class Model(FEAData): Attributes ---------- - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. description : str Some description of the model. This will be added to the input file and can be useful for future reference. @@ -66,7 +56,7 @@ class Model(FEAData): The constraints of the model. partgroups : Set[:class:`compas_fea2.model.PartsGroup`] The part groups of the model. - materials : Set[:class:`compas_fea2.model.materials._Material] + materials : Set[:class:`compas_fea2.model.materials.Material] The materials assigned in the model. sections : Set[:class:`compas_fea2.model._Section] The sections assigned in the model. @@ -74,10 +64,11 @@ class Model(FEAData): The problems added to the model. path : ::class::`pathlib.Path` Path to the main folder where the problems' results are stored. + """ - def __init__(self, *, name=None, description=None, author=None, **kwargs): - super(Model, self).__init__(name=name, **kwargs) + def __init__(self, description=None, author=None, **kwargs): + super(Model, self).__init__(**kwargs) self.description = description self.author = author self._parts = set() @@ -137,36 +128,37 @@ def loads(self): @property def path(self): return self._path + @path.setter def path(self, value): if not isinstance(value, Path): try: value = Path(value) - except: - raise ValueError('the path provided is not valid.') + except Exception: + raise ValueError("the path provided is not valid.") self._path = value.joinpath(self.name) @property def nodes(self): - node_set=set() + node_set = set() for part in self.parts: node_set.update(part.nodes) return node_set @property def elements(self): - element_set=set() + element_set = set() for part in self.parts: element_set.update(part.elements) return element_set + # ========================================================================= # Constructor methods # ========================================================================= @staticmethod - @timer(message='Model loaded from cfm file in ') + @timer(message="Model loaded from cfm file in ") def from_cfm(path): - # type: (str) -> Model """Imports a Problem object from an .cfm file through Pickle. Parameters @@ -178,19 +170,20 @@ def from_cfm(path): ------- :class:`compas_fea2.model.Model` The imported model. + """ - with open(path, 'rb') as f: + with open(path, "rb") as f: try: # disable garbage collector gc.disable() model = pickle.load(f) # enable garbage collector again gc.enable() - except: + except Exception: gc.enable() - raise RuntimeError('Model not created!') + raise RuntimeError("Model not created!") model.path = os.sep.join(os.path.split(path)[0].split(os.sep)[:-1]) - #check if the problems' results are stored in the same location + # check if the problems' results are stored in the same location for problem in model.problems: if not os.path.exists(os.path.join(model.path, problem.name)): print(f"WARNING! - Problem {problem.name} results not found! move the results folder in {model.path}") @@ -207,7 +200,6 @@ def to_json(self): raise NotImplementedError() def to_cfm(self, path): - # type: (Path) -> None """Exports the Model object to an .cfm file through Pickle. Parameters @@ -218,22 +210,22 @@ def to_cfm(self, path): Returns ------- None + """ if not isinstance(path, Path): path = Path(path) - if not path.suffix == '.cfm': + if not path.suffix == ".cfm": raise ValueError("Please provide a valid path including the name of the file.") pathlib.Path(path.parent.absolute()).mkdir(parents=True, exist_ok=True) - with open(path, 'wb') as f: + with open(path, "wb") as f: pickle.dump(self, f) - print('Model saved to: {}'.format(path)) + print("Model saved to: {}".format(path)) # ========================================================================= # Parts methods # ========================================================================= def find_part_by_name(self, name, casefold=False): - # type: (str, bool) -> DeformablePart """Find if there is a part with a given name in the model. Parameters @@ -255,7 +247,6 @@ def find_part_by_name(self, name, casefold=False): return part def contains_part(self, part): - # type: (DeformablePart) -> DeformablePart """Verify that the model contains a specific part. Parameters @@ -270,16 +261,15 @@ def contains_part(self, part): return part in self.parts def add_part(self, part): - # type: (DeformablePart) -> DeformablePart """Adds a DeformablePart to the Model. Parameters ---------- - part : :class:`compas_fea2.model._Part` + part : :class:`compas_fea2.model.Part` Returns ------- - :class:`compas_fea2.model._Part` + :class:`compas_fea2.model.Part` Raises ------ @@ -287,7 +277,7 @@ def add_part(self, part): If the part is not a part. """ - if not isinstance(part, _Part): + if not isinstance(part, Part): raise TypeError("{!r} is not a part.".format(part)) if self.contains_part(part): @@ -314,7 +304,6 @@ def add_part(self, part): return part def add_parts(self, parts): - # type: (list) -> list """Add multiple parts to the model. Parameters @@ -332,58 +321,58 @@ def add_parts(self, parts): # Nodes methods # ========================================================================= - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_node_by_key(self, key): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_by_name(self, name): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_by_location(self, point, distance, plane=None): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_closest_nodes_to_point(self, point, distance, number_of_nodes=1, plane=None): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_around_node(self, node, distance): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_closest_nodes_to_node(self, node, distance, number_of_nodes=1, plane=None): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_by_attribute(self, attr, value, tolerance): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_on_plane(self, plane): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_in_polygon(self, polygon, tolerance=1.1): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_nodes_where(self, conditions): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def contains_node(self, node): pass @@ -391,19 +380,20 @@ def contains_node(self, node): # Nodes methods # ========================================================================= - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_element_by_key(self, key): pass - @get_docstring(_Part) - @part_method + # @get_docstring(Part) + # @part_method def find_elements_by_name(self, name): pass # ========================================================================= # Groups methods # ========================================================================= + def add_parts_group(self, group): """Add a PartsGroup object to the Model. @@ -416,9 +406,10 @@ def add_parts_group(self, group): ------- :class:`compas_fea2.model.PartsGroup` The added group. + """ if not isinstance(group, PartsGroup): - raise TypeError('Only PartsGroups can be added to a model') + raise TypeError("Only PartsGroups can be added to a model") self.partgroups.add(group) group._registration = self # FIXME wrong because the members of the group might have a different registation return group @@ -435,6 +426,7 @@ def add_parts_groups(self, groups): ------- [:class:`compas_fea2.model.PartsGroup`] The list with the added groups. + """ return [self.add_parts_group(group) for group in groups] @@ -452,66 +444,75 @@ def group_parts_where(self, attr, value): ------- :class:`compas_fea2.model.PartsGroup` The group with the matching parts. + """ return self.add_parts_group(PartsGroup(parts=set(filter(lambda p: getattr(p, attr) == value), self.parts))) - # ========================================================================= # BCs methods # ========================================================================= - def add_bcs(self, bc, nodes, axes='global'): - # type: (_BoundaryCondition, Node, str) -> _BoundaryCondition - """Add a :class:`compas_fea2.model._BoundaryCondition` to the model. - - Note - ---- - Currently global axes are used in the Boundary Conditions definition. + def add_bcs(self, bc, nodes, axes="global"): + """Add a :class:`compas_fea2.model.BoundaryCondition` to the model. Parameters ---------- - bc : :class:`compas_fea2.model._BoundaryCondition` + bc : :class:`compas_fea2.model.BoundaryCondition` Boundary condition object to add to the model. nodes : list[:class:`compas_fea2.model.Node`] or :class:`compas_fea2.model.NodesGroup` List or Group with the nodes where the boundary condition is assigned. Returns ------- - :class:`compas_fea2.model._BoundaryCondition` + :class:`compas_fea2.model.BoundaryCondition` + + Notes + ----- + Currently global axes are used in the Boundary Conditions definition. """ - if isinstance(nodes, _Group): + if isinstance(nodes, Group): nodes = nodes._members if isinstance(nodes, Node): nodes = [nodes] - if not isinstance(bc, _BoundaryCondition): - raise TypeError('{!r} is not a Boundary Condition.'.format(bc)) + if not isinstance(bc, BoundaryCondition): + raise TypeError("{!r} is not a Boundary Condition.".format(bc)) for node in nodes: if not isinstance(node, Node): - raise TypeError('{!r} is not a Node.'.format(node)) + raise TypeError("{!r} is not a Node.".format(node)) if not node.part: - raise ValueError('{!r} is not registered to any part.'.format(node)) - elif not node.part in self.parts: - raise ValueError('{!r} belongs to a part not registered to this model.'.format(node)) + raise ValueError("{!r} is not registered to any part.".format(node)) + elif node.part not in self.parts: + raise ValueError("{!r} belongs to a part not registered to this model.".format(node)) if isinstance(node.part, RigidPart): if len(nodes) != 1 or not node.is_reference: - raise ValueError('For rigid parts bundary conditions can be assigned only to the reference point') + raise ValueError("For rigid parts bundary conditions can be assigned only to the reference point") node._bc = bc - self._bcs[bc]=set(nodes) + self._bcs[bc] = set(nodes) bc._registration = self return bc - def _add_bc_type(self, bc_type, nodes, axes='global'): - # type: (str, Node, str) -> _BoundaryCondition - """Add a :class:`compas_fea2.model._BoundaryCondition` by type. + def _add_bc_type(self, bc_type, nodes, axes="global"): + """Add a :class:`compas_fea2.model.BoundaryCondition` by type. - Note - ---- + Parameters + ---------- + name : str + name of the boundary condition + bc_type : str + one of the boundary condition types specified above + nodes : list[:class:`compas_fea2.model.Node`] or :class:`compas_fea2.model.NodesGroup` + List or Group with the nodes where the boundary condition is assigned. + axes : str, optional + [axes of the boundary condition, by default 'global' + + Notes + ----- The bc_type must be one of the following: .. csv-table:: @@ -529,29 +530,25 @@ def _add_bc_type(self, bc_type, nodes, axes='global'): rollerYZ, :class:`compas_fea2.model.bcs.RollerBCYZ` rollerXZ, :class:`compas_fea2.model.bcs.RollerBCXZ` - - Parameters - ---------- - name : str - name of the boundary condition - bc_type : str - one of the boundary condition types specified above - nodes : list[:class:`compas_fea2.model.Node`] or :class:`compas_fea2.model.NodesGroup` - List or Group with the nodes where the boundary condition is assigned. - axes : str, optional - [axes of the boundary condition, by default 'global' """ - types = {'fix': 'FixedBC', 'fixXX': 'FixedBCXX', 'fixYY': 'FixedBCYY', - 'fixZZ': 'FixedBCZZ', 'pin': 'PinnedBC', 'rollerX': 'RollerBCX', - 'rollerY': 'RollerBCY', 'rollerZ': 'RollerBCZ', 'rollerXY': 'RollerBCXY', - 'rollerYZ': 'RollerBCYZ', 'rollerXZ': 'RollerBCXZ', - } - m = importlib.import_module('compas_fea2.model.bcs') + types = { + "fix": "FixedBC", + "fixXX": "FixedBCXX", + "fixYY": "FixedBCYY", + "fixZZ": "FixedBCZZ", + "pin": "PinnedBC", + "rollerX": "RollerBCX", + "rollerY": "RollerBCY", + "rollerZ": "RollerBCZ", + "rollerXY": "RollerBCXY", + "rollerYZ": "RollerBCYZ", + "rollerXZ": "RollerBCXZ", + } + m = importlib.import_module("compas_fea2.model.bcs") bc = getattr(m, types[bc_type])() return self.add_bcs(bc, nodes, axes) - def add_fix_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_fix_bc(self, nodes, axes="global"): """Add a :class:`compas_fea2.model.FixedBC` to the nodes in a part. Parameters @@ -562,11 +559,11 @@ def add_fix_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('fix', nodes, axes) + return self._add_bc_type("fix", nodes, axes) - def add_pin_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_pin_bc(self, nodes, axes="global"): """Add a pinned boundary condition type to some nodes in a part. Parameters @@ -577,11 +574,11 @@ def add_pin_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('pin', nodes, axes) + return self._add_bc_type("pin", nodes, axes) - def add_clampXX_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_clampXX_bc(self, nodes, axes="global"): """Add a fixed boundary condition type free about XX to some nodes in a part. Parameters @@ -592,11 +589,11 @@ def add_clampXX_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('clampXX', nodes, axes) + return self._add_bc_type("clampXX", nodes, axes) - def add_clampYY_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_clampYY_bc(self, nodes, axes="global"): """Add a fixed boundary condition free about YY type to some nodes in a part. Parameters @@ -607,11 +604,11 @@ def add_clampYY_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('clampYY', nodes, axes) + return self._add_bc_type("clampYY", nodes, axes) - def add_clampZZ_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_clampZZ_bc(self, nodes, axes="global"): """Add a fixed boundary condition free about ZZ type to some nodes in a part. Parameters @@ -622,11 +619,11 @@ def add_clampZZ_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('clampZZ', nodes, axes) + return self._add_bc_type("clampZZ", nodes, axes) - def add_rollerX_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerX_bc(self, nodes, axes="global"): """Add a roller free on X boundary condition type to some nodes in a part. Parameters @@ -637,11 +634,11 @@ def add_rollerX_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerX', nodes, axes) + return self._add_bc_type("rollerX", nodes, axes) - def add_rollerY_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerY_bc(self, nodes, axes="global"): """Add a roller free on Y boundary condition type to some nodes in a part. Parameters @@ -652,11 +649,11 @@ def add_rollerY_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerY', nodes, axes) + return self._add_bc_type("rollerY", nodes, axes) - def add_rollerZ_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerZ_bc(self, nodes, axes="global"): """Add a roller free on Z boundary condition type to some nodes in a part. Parameters @@ -667,11 +664,11 @@ def add_rollerZ_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerZ', nodes, axes) + return self._add_bc_type("rollerZ", nodes, axes) - def add_rollerXY_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerXY_bc(self, nodes, axes="global"): """Add a roller free on XY boundary condition type to some nodes in a part. Parameters @@ -682,11 +679,11 @@ def add_rollerXY_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerXY', nodes, axes) + return self._add_bc_type("rollerXY", nodes, axes) - def add_rollerXZ_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerXZ_bc(self, nodes, axes="global"): """Add a roller free on XZ boundary condition type to some nodes in a part. Parameters @@ -697,11 +694,11 @@ def add_rollerXZ_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerXZ', nodes, axes) + return self._add_bc_type("rollerXZ", nodes, axes) - def add_rollerYZ_bc(self, nodes, axes='global'): - # type: (Node, str) -> _BoundaryCondition + def add_rollerYZ_bc(self, nodes, axes="global"): """Add a roller free on YZ boundary condition type to some nodes in a part. Parameters @@ -712,8 +709,9 @@ def add_rollerYZ_bc(self, nodes, axes='global'): List or Group with the nodes where the boundary condition is assigned. axes : str, optional [axes of the boundary condition, by default 'global' + """ - return self._add_bc_type('rollerYZ', nodes, axes) + return self._add_bc_type("rollerYZ", nodes, axes) def remove_bcs(self, nodes): """Release a node previously restrained. @@ -726,6 +724,7 @@ def remove_bcs(self, nodes): Returns ------- None + """ if isinstance(nodes, Node): @@ -738,7 +737,6 @@ def remove_bcs(self, nodes): else: print("WARNING: {!r} was not restrained. skipped!".format(node)) - def remove_all_bcs(self): """Removes all the boundary conditions from the Model. @@ -749,6 +747,7 @@ def remove_all_bcs(self): Returns ------- None + """ for _, nodes in self.bcs.items(): self.remove_bcs(nodes) @@ -759,32 +758,31 @@ def remove_all_bcs(self): # ============================================================================== def _add_ics(self, ic, group): - # type: (_InitialCondition, _Group, str) -> list - """Add a :class:`compas_fea2.model._InitialCondition` to the model. + """Add a :class:`compas_fea2.model.InitialCondition` to the model. Parameters ---------- - ic : :class:`compas_fea2.model._InitialCondition` + ic : :class:`compas_fea2.model.InitialCondition` Initial condition object to add to the model. - group : :class:`compas_fea2.model._Group` + group : :class:`compas_fea2.model.Group` Group of Nodes/Elements where the initial condition is assigned. Returns ------- - :class:`compas_fea2.model._InitialCondition` + :class:`compas_fea2.model.InitialCondition` """ group.part.add_group(group) - if not isinstance(ic, _InitialCondition): - raise TypeError('{!r} is not a _InitialCondition.'.format(ic)) + if not isinstance(ic, InitialCondition): + raise TypeError("{!r} is not a InitialCondition.".format(ic)) for member in group.members: - if not isinstance(member, (Node, _Element)): - raise TypeError('{!r} is not a Node or an Element.'.format(member)) + if not isinstance(member, (Node, Element)): + raise TypeError("{!r} is not a Node or an Element.".format(member)) if not member.part: - raise ValueError('{!r} is not registered to any part.'.format(member)) - elif not member.part in self.parts: - raise ValueError('{!r} belongs to a part not registered to this model.'.format(member)) + raise ValueError("{!r} is not registered to any part.".format(member)) + elif member.part not in self.parts: + raise ValueError("{!r} belongs to a part not registered to this model.".format(member)) member._ic = ic self._ics[ic] = group.members @@ -792,55 +790,51 @@ def _add_ics(self, ic, group): return ic - def add_nodes_ics(self, ic, nodes): - # type: (_InitialCondition, Node, str) -> list - """Add a :class:`compas_fea2.model._InitialCondition` to the model. + """Add a :class:`compas_fea2.model.InitialCondition` to the model. Parameters ---------- - ic : :class:`compas_fea2.model._InitialCondition` + ic : :class:`compas_fea2.model.InitialCondition` Initial condition object to add to the model. nodes : list[:class:`compas_fea2.model.Node`] or :class:`compas_fea2.model.NodesGroup` List or Group with the nodes where the initial condition is assigned. Returns ------- - list[:class:`compas_fea2.model._InitialCondition`] + list[:class:`compas_fea2.model.InitialCondition`] """ if not isinstance(nodes, NodesGroup): - raise TypeError('{} is not a group of nodes'.format(nodes)) + raise TypeError("{} is not a group of nodes".format(nodes)) self._add_ics(ic, nodes) return ic def add_elements_ics(self, ic, elements): - # type: (_InitialCondition, Node, str) -> list - """Add a :class:`compas_fea2.model._InitialCondition` to the model. + """Add a :class:`compas_fea2.model.InitialCondition` to the model. Parameters ---------- - ic : :class:`compas_fea2.model._InitialCondition` + ic : :class:`compas_fea2.model.InitialCondition` Initial condition object to add to the model. elements : :class:`compas_fea2.model.ElementsGroup` List or Group with the elements where the initial condition is assigned. Returns ------- - :class:`compas_fea2.model._InitialCondition` + :class:`compas_fea2.model.InitialCondition` """ if not isinstance(elements, ElementsGroup): - raise TypeError('{} is not a group of elements'.format(elements)) + raise TypeError("{} is not a group of elements".format(elements)) self._add_ics(ic, elements) return ic # ============================================================================== # Summary # ============================================================================== - # TODO add shor/ long + def summary(self): - # type: () -> str """Prints a summary of the Model object. Parameters @@ -851,25 +845,41 @@ def summary(self): ------- str Model summary - """ - parts_info = ['\n'.join(['{}'.format(part.name), - ' # of nodes: {}'.format(len(part.nodes)), - ' # of elements: {}'.format(len(part.elements)), - ' is_rigid : {}'.format('True' if isinstance(part, RigidPart) else 'False')]) for part in self.parts] - constraints_info = '\n'.join([e.__repr__() for e in self.constraints]) + """ + parts_info = [ + "\n".join( + [ + "{}".format(part.name), + " # of nodes: {}".format(len(part.nodes)), + " # of elements: {}".format(len(part.elements)), + " is_rigid : {}".format("True" if isinstance(part, RigidPart) else "False"), + ] + ) + for part in self.parts + ] + + constraints_info = "\n".join([e.__repr__() for e in self.constraints]) bc_info = [] for bc, nodes in self.bcs.items(): for part, part_nodes in groupby(nodes, lambda n: n.part): - bc_info.append('{}: \n{}'.format(part.name, '\n'.join([' {!r} - # of restrained nodes {}'.format(bc, len(list(part_nodes)))]))) - bc_info = '\n'.join(bc_info) + bc_info.append( + "{}: \n{}".format( + part.name, "\n".join([" {!r} - # of restrained nodes {}".format(bc, len(list(part_nodes)))]) + ) + ) + bc_info = "\n".join(bc_info) ic_info = [] for ic, nodes in self.ics.items(): for part, part_nodes in groupby(nodes, lambda n: n.part): - ic_info.append('{}: \n{}'.format(part.name, '\n'.join([' {!r} - # of restrained nodes {}'.format(ic, len(list(part_nodes)))]))) - ic_info = '\n'.join(ic_info) + ic_info.append( + "{}: \n{}".format( + part.name, "\n".join([" {!r} - # of restrained nodes {}".format(ic, len(list(part_nodes)))]) + ) + ) + ic_info = "\n".join(ic_info) data = """ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -894,14 +904,15 @@ def summary(self): Initial Conditions ------------------ {} -""".format(self.name, - self.description or 'N/A', - self.author or 'N/A', - '\n'.join(parts_info), - constraints_info or 'N/A', - bc_info or 'N/A', - ic_info or 'N/A' - ) +""".format( + self.name, + self.description or "N/A", + self.author or "N/A", + "\n".join(parts_info), + constraints_info or "N/A", + bc_info or "N/A", + ic_info or "N/A", + ) print(data) return data @@ -909,32 +920,31 @@ def summary(self): # Save model file # ============================================================================== - def check(self, type='quick'): + def check(self, type="quick"): """Check for possible problems in the model - Warning - ------- - WIP! It is better if you check yourself... + Parameters + ---------- + type : str, optional + *quick* or *deep* check, by default 'quick' + + Returns + ------- + str + report - Parameters - ---------- - type : str, optional - *quick* or *deep* check, by default 'quick' + Warnings + -------- + WIP! It is better if you check yourself... - Returns - ------- - str - report - """ + """ def _check_units(self): - """Check if the units are consistent. - """ + """Check if the units are consistent.""" raise NotImplementedError def _check_bcs(self): - """Check if the units are consistent. - """ + """Check if the units are consistent.""" raise NotImplementedError raise NotImplementedError @@ -960,9 +970,10 @@ def add_problem(self, problem): ------ TypeError if problem is not type :class:`compas_fea2.problem.Problem` + """ if not isinstance(problem, compas_fea2.problem.Problem): - raise TypeError('{} is not a Problem'.format(problem)) + raise TypeError("{} is not a Problem".format(problem)) self._problems.add(problem) problem._registration = self return problem @@ -984,6 +995,7 @@ def add_problems(self, problems): ------ TypeError if a problem is not type :class:`compas_fea2.problem.Problem` + """ return [self.add_problem(problem) for problem in problems] @@ -999,6 +1011,7 @@ def find_problem_by_name(self, name): ------- :class:`compas_fea2.problem.Problem` The problem + """ for problem in self.problems: if problem.name == name: @@ -1009,84 +1022,96 @@ def find_problem_by_name(self, name): # ========================================================================= # @get_docstring(Problem) - @problem_method + # # @problem_method def write_input_file(self, problems=None, path=None, *args, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def analyse(self, problems=None, *args, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def analyze(self, problems=None, *args, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def restart_analysis(self, problem, start, steps, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def analyse_and_extract(self, problems=None, path=None, *args, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def analyse_and_store(self, problems=None, memory_only=False, *args, **kwargs): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def store_results_in_model(self, problems=None, *args, **kwargs): pass # ============================================================================== # Results methods # ============================================================================== - #@get_docstring(Problem) - @problem_method + + # @get_docstring(Problem) + # # @problem_method def get_reaction_forces_sql(self, *, problem=None, step=None): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # # @problem_method def get_reaction_moments_sql(self, problem, step=None): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # @problem_method def get_displacements_sql(self, problem, step=None): pass - #@get_docstring(Problem) - @problem_method - def get_max_displacement_sql(self, problem, step=None, component='magnitude'): + # @get_docstring(Problem) + # @problem_method + def get_max_displacement_sql(self, problem, step=None, component="magnitude"): pass - #@get_docstring(Problem) - @problem_method - def get_min_displacement_sql(self, problem, step=None, component='magnitude'): + # @get_docstring(Problem) + # @problem_method + def get_min_displacement_sql(self, problem, step=None, component="magnitude"): pass - #@get_docstring(Problem) - @problem_method + # @get_docstring(Problem) + # @problem_method def get_displacement_at_nodes_sql(self, problem, nodes, steps=None): pass - @problem_method + # @problem_method def get_displacement_at_point_sql(self, problem, point, steps=None): pass # ============================================================================== # Viewer # ============================================================================== - def show(self, width=1600, height=900, scale_factor=1., parts=None, - solid=True, draw_nodes=False, node_labels=False, - draw_bcs=1., draw_constraints=True, **kwargs): - """WIP + + def show( + self, + width=1600, + height=900, + scale_factor=1.0, + parts=None, + solid=True, + draw_nodes=False, + node_labels=False, + draw_bcs=1.0, + draw_constraints=True, + **kwargs, + ): + """Visualise the model in the viewer. Parameters ---------- @@ -1108,19 +1133,16 @@ def show(self, width=1600, height=900, scale_factor=1., parts=None, _description_, by default 1. draw_constraints : bool, optional _description_, by default True + """ from compas_fea2.UI.viewer import FEA2Viewer - from compas.geometry import Point, Vector parts = parts or self.parts v = FEA2Viewer(width, height, scale_factor=scale_factor) - v.draw_parts(parts, - draw_nodes, - node_labels, - solid) + v.draw_parts(parts, draw_nodes, node_labels, solid) if draw_bcs: v.draw_bcs(self, parts, draw_bcs) @@ -1130,6 +1152,6 @@ def show(self, width=1600, height=900, scale_factor=1., parts=None, v.show() - @problem_method + # @problem_method def show_displacements(self, problem, *args, **kwargs): pass diff --git a/src/compas_fea2/model/nodes.py b/src/compas_fea2/model/nodes.py index 1a01108ce..aa307467e 100644 --- a/src/compas_fea2/model/nodes.py +++ b/src/compas_fea2/model/nodes.py @@ -6,17 +6,11 @@ from compas.geometry import Point from compas_fea2.base import FEAData -from .bcs import _BoundaryCondition import compas_fea2 -class Node(FEAData): - """Initialises base Node object. - Note - ---- - Nodes are registered to a :class:`compas_fea2.model.DeformablePart` object and can - belong to only one Part. Every time a node is added to a Part, it gets - registered to that Part. +class Node(FEAData): + """Class representing a Node object. Parameters ---------- @@ -69,6 +63,12 @@ class Node(FEAData): temperature : float The temperature at the Node. + Notes + ----- + Nodes are registered to a :class:`compas_fea2.model.DeformablePart` object and can + belong to only one Part. Every time a node is added to a Part, it gets + registered to that Part. + Examples -------- >>> node = Node(xyz=(1.0, 2.0, 3.0)) @@ -85,9 +85,9 @@ def __init__(self, xyz, mass=None, temperature=None, name=None, **kwargs): self._z = xyz[2] self._bc = None - self._dof = {'x': True, 'y': True, 'z': True, 'xx': True, 'yy': True, 'zz': True} + self._dof = {"x": True, "y": True, "z": True, "xx": True, "yy": True, "zz": True} - self._mass = mass if isinstance(mass, tuple) else tuple([mass]*3) + self._mass = mass if isinstance(mass, tuple) else tuple([mass] * 3) self._temperature = temperature self._on_boundary = None @@ -115,8 +115,8 @@ def xyz(self): @xyz.setter def xyz(self, value): - if len(value)!=3: - raise ValueError('Provide a 3 element touple or list') + if len(value) != 3: + raise ValueError("Provide a 3 element touple or list") self._x = value[0] self._y = value[1] self._z = value[2] @@ -151,7 +151,7 @@ def mass(self): @mass.setter def mass(self, value): - self._mass = value if isinstance(value, tuple) else tuple([value]*3) + self._mass = value if isinstance(value, tuple) else tuple([value] * 3) @property def temperature(self): @@ -168,7 +168,7 @@ def gkey(self): @property def dof(self): if self.bc: - return {attr: not bool(getattr(self.bc, attr)) for attr in ['x', 'y', 'z', 'xx', 'yy', 'zz']} + return {attr: not bool(getattr(self.bc, attr)) for attr in ["x", "y", "z", "xx", "yy", "zz"]} else: return self._dof diff --git a/src/compas_fea2/model/parts.py b/src/compas_fea2/model/parts.py index f67b00ccb..5f7b0b2bb 100644 --- a/src/compas_fea2/model/parts.py +++ b/src/compas_fea2/model/parts.py @@ -3,35 +3,38 @@ from __future__ import print_function from math import sqrt -from compas.geometry import Point, Plane, Frame, Polygon +from compas.geometry import Point, Plane, Frame from compas.geometry import Transformation, Scale -from compas.geometry import normalize_vector from compas.geometry import distance_point_point_sqrd from compas.geometry import is_point_in_polygon_xy from compas.geometry import is_point_on_plane from compas.utilities import geometric_key from compas.geometry import Vector -from compas.geometry import sum_vectors import compas_fea2 from compas_fea2.base import FEAData from .nodes import Node -from .elements import _Element, _Element2D, _Element1D, _Element3D, BeamElement, HexahedronElement, ShellElement, TetrahedronElement, Face -from .materials import _Material -from .sections import _Section, ShellSection, SolidSection -from .releases import _BeamEndRelease, BeamEndPinRelease +from .elements import ( + Element, + Element2D, + Element1D, + Element3D, + BeamElement, + HexahedronElement, + ShellElement, + TetrahedronElement, +) +from .materials import Material +from .sections import Section, ShellSection, SolidSection +from .releases import BeamEndRelease from .groups import NodesGroup, ElementsGroup, FacesGroup -from .ics import InitialStressField from compas_fea2.utilities._utils import timer -class _Part(FEAData): - """ - Note - ---- - Parts are registered to a :class:`compas_fea2.model.Model`. +class Part(FEAData): + """Base class for Parts. Parameters ---------- @@ -52,13 +55,13 @@ class _Part(FEAData): Number of nodes in the part. gkey_node : {gkey : :class:`compas_fea2.model.Node`} Dictionary that associates each node and its geometric key} - materials : Set[:class:`compas_fea2.model._Material`] + materials : Set[:class:`compas_fea2.model.Material`] The materials belonging to the part. - sections : Set[:class:`compas_fea2.model._Section`] + sections : Set[:class:`compas_fea2.model.Section`] The sections belonging to the part. - elements : Set[:class:`compas_fea2.model._Element`] + elements : Set[:class:`compas_fea2.model.Element`] The elements belonging to the part. - element_types : {:class:`compas_fea2.model._Element` : [:class:`compas_fea2.model._Element`]] + element_types : {:class:`compas_fea2.model.Element` : [:class:`compas_fea2.model.Element`]] Dictionary with the elements of the part for each element type. element_count : int Number of elements in the part @@ -72,10 +75,15 @@ class _Part(FEAData): The outer boundary mesh enveloping the Part. discretized_boundary_mesh : :class:`compas.datastructures.Mesh` The discretized outer boundary mesh enveloping the Part. + + Notes + ----- + Parts are registered to a :class:`compas_fea2.model.Model`. + """ def __init__(self, name=None, **kwargs): - super(_Part, self).__init__(name=name, **kwargs) + super(Part, self).__init__(name=name, **kwargs) self._nodes = set() self._gkey_node = {} self._sections = set() @@ -137,7 +145,7 @@ def discretized_boundary_mesh(self): @property def volume(self): - self._volume = 0. + self._volume = 0.0 for element in self.elements: if element.volume: self._volume += element.volume @@ -153,40 +161,39 @@ def results(self): @property def nodes_count(self): - return len(self.nodes)-1 + return len(self.nodes) - 1 @property def elements_count(self): - return len(self.elements)-1 + return len(self.elements) - 1 @property def element_types(self): element_types = {} for element in self.elements: - element_types.setdefault(type(element),[]).append(element) + element_types.setdefault(type(element), []).append(element) return element_types + # def __str__(self): + # return """ + # {} + # {} + # name : {} -# def __str__(self): -# return """ -# {} -# {} -# name : {} - -# number of elements : {} -# number of nodes : {} -# """.format(self.__class__.__name__, -# len(self.__class__.__name__) * '-', -# self.name, -# self.elements_count, -# self.nodes_count) + # number of elements : {} + # number of nodes : {} + # """.format(self.__class__.__name__, + # len(self.__class__.__name__) * '-', + # self.name, + # self.elements_count, + # self.nodes_count) # ========================================================================= # Constructor methods # ========================================================================= @classmethod - def from_compas_line(cls, line, element_model='BeamElement', section=None, name=None, **kwargs): + def from_compas_line(cls, line, element_model="BeamElement", section=None, name=None, **kwargs): """Generate a part from a class:`compas.geometry.Line`. Parameters @@ -204,19 +211,22 @@ def from_compas_line(cls, line, element_model='BeamElement', section=None, name= ------- class:`compas_fea2.model.Part` The part. + """ import compas_fea2 + prt = cls(name=name) element = getattr(compas_fea2.model, element_model)(nodes=[Node(line.start), Node(line.end)], section=section) - if not isinstance(element, _Element1D): + if not isinstance(element, Element1D): raise ValueError("Provide a 1D element") prt.add_element(element) return prt @classmethod - @timer(message='compas Mesh successfully imported in ') + @timer(message="compas Mesh successfully imported in ") def shell_from_compas_mesh(cls, mesh, section, name=None, **kwargs): """Creates a DeformablePart object from a :class:`compas.datastructures.Mesh`. + To each face of the mesh is assigned a :class:`compas_fea2.model.ShellElement` objects. Currently, the same section is applied to all the elements. @@ -231,7 +241,7 @@ def shell_from_compas_mesh(cls, mesh, section, name=None, **kwargs): automatically. """ - implementation = kwargs.get('implementation', None) + implementation = kwargs.get("implementation", None) part = cls(name, **kwargs) vertex_node = {vertex: part.add_node(Node(mesh.vertex_coordinates(vertex))) for vertex in mesh.vertices()} @@ -249,19 +259,11 @@ def shell_from_compas_mesh(cls, mesh, section, name=None, **kwargs): @classmethod # @timer(message='part successfully imported from gmsh model in ') def from_gmsh(cls, gmshModel, name=None, **kwargs): - """Create a Part object from a gmshModel object. According to the - `section` type provided, :class:`compas_fea2.model._Element2D` or - :class:`compas_fea2.model._Element3D` elements are cretated. - The same section is applied to all the elements. - - Note - ---- - The gmshModel must have the right dimension corresponding to the section - provided. + """Create a Part object from a gmshModel object. - Warning - ------- - the `split` option is currently not implemented + According to the `section` type provided, :class:`compas_fea2.model.Element2D` or + :class:`compas_fea2.model.Element3D` elements are cretated. + The same section is applied to all the elements. Parameters ---------- @@ -284,9 +286,13 @@ def from_gmsh(cls, gmshModel, name=None, **kwargs): Returns ------- - :class:`compas_fea2.model._Part` + :class:`compas_fea2.model.Part` The part meshed. + Notes + ----- + The gmshModel must have the right dimension corresponding to the section provided. + References ---------- .. [1] https://gitlab.onelab.info/gmsh/gmsh/blob/gmsh_4_9_1/api/gmsh.py @@ -304,47 +310,44 @@ def from_gmsh(cls, gmshModel, name=None, **kwargs): part = cls(name=name) # add nodes gmsh_nodes = gmshModel.mesh.get_nodes() - node_coords = gmsh_nodes[1].reshape((-1, 3), order='C') + node_coords = gmsh_nodes[1].reshape((-1, 3), order="C") fea2_nodes = [part.add_node(Node(coords.tolist())) for coords in node_coords] # add elements gmsh_elements = gmshModel.mesh.get_elements() - section = kwargs.get('section', None) - split = kwargs.get('split', False) - verbose = kwargs.get('verbose', False) - rigid = kwargs.get('rigid', False) - implementation = kwargs.get('implementation', None) + section = kwargs.get("section", None) + split = kwargs.get("split", False) + verbose = kwargs.get("verbose", False) + rigid = kwargs.get("rigid", False) + implementation = kwargs.get("implementation", None) dimension = 2 if isinstance(section, SolidSection) else 1 - ntags_per_element = np.split(gmsh_elements[2][dimension]-1, - len(gmsh_elements[1][dimension])) # gmsh keys start from 1 + ntags_per_element = np.split( + gmsh_elements[2][dimension] - 1, len(gmsh_elements[1][dimension]) + ) # gmsh keys start from 1 for ntags in ntags_per_element: if split: - raise NotImplementedError('this feature is under development') + raise NotImplementedError("this feature is under development") element_nodes = [fea2_nodes[ntag] for ntag in ntags] if ntags.size == 3: - k = part.add_element(ShellElement(nodes=element_nodes, - section=section, - rigid=rigid, - implementation=implementation)) + k = part.add_element( + ShellElement(nodes=element_nodes, section=section, rigid=rigid, implementation=implementation) + ) elif ntags.size == 4: if isinstance(section, ShellSection): - k = part.add_element(ShellElement(nodes=element_nodes, - section=section, - rigid=rigid, - implementation=implementation)) + k = part.add_element( + ShellElement(nodes=element_nodes, section=section, rigid=rigid, implementation=implementation) + ) else: - k = part.add_element(TetrahedronElement(nodes=element_nodes, - section=section)) + k = part.add_element(TetrahedronElement(nodes=element_nodes, section=section)) elif ntags.size == 8: - k = part.add_element(HexahedronElement(nodes=element_nodes, - section=section)) + k = part.add_element(HexahedronElement(nodes=element_nodes, section=section)) else: - raise NotImplementedError('Element with {} nodes not supported'.format(ntags.size)) + raise NotImplementedError("Element with {} nodes not supported".format(ntags.size)) if verbose: - print('element {} added'.format(k)) + print("element {} added".format(k)) return part @@ -369,11 +372,12 @@ def from_boundary_mesh(cls, boundary_mesh, name=None, **kwargs): """ from compas_gmsh.models import MeshModel - target_mesh_size = kwargs.get('target_mesh_size', 1) - mesh_size_at_vertices = kwargs.get('mesh_size_at_vertices', None) - target_point_mesh_size = kwargs.get('target_point_mesh_size', None) - meshsize_max = kwargs.get('meshsize_max', None) - meshsize_min = kwargs.get('meshsize_min', None) + + target_mesh_size = kwargs.get("target_mesh_size", 1) + mesh_size_at_vertices = kwargs.get("mesh_size_at_vertices", None) + target_point_mesh_size = kwargs.get("target_point_mesh_size", None) + meshsize_max = kwargs.get("meshsize_max", None) + meshsize_min = kwargs.get("meshsize_min", None) gmshModel = MeshModel.from_mesh(boundary_mesh, targetlength=target_mesh_size) @@ -401,9 +405,9 @@ def from_boundary_mesh(cls, boundary_mesh, name=None, **kwargs): gmshModel.generate_mesh(2) part._discretized_boundary_mesh = gmshModel.mesh_to_compas() - del(gmshModel) + del gmshModel - if kwargs.get('rigid', False): + if kwargs.get("rigid", False): point = boundary_mesh.centroid() part.reference_point = Node(xyz=[point.x, point.y, point.z]) @@ -417,6 +421,7 @@ def from_boundary_mesh(cls, boundary_mesh, name=None, **kwargs): # ========================================================================= # Nodes methods # ========================================================================= + def find_node_by_key(self, key): # type: (int) -> Node """Retrieve a node in the model using its key. @@ -430,6 +435,7 @@ def find_node_by_key(self, key): ------- :class:`compas_fea2.model.Node` The corresponding node. + """ for node in self.nodes: if node.key == key: @@ -471,12 +477,11 @@ def find_nodes_by_location(self, point, distance, plane=None, report=False, **kw list[:class:`compas_fea2.model.Node`] """ - d2 = distance ** 2 + d2 = distance**2 nodes = self.find_nodes_on_plane(plane) if plane else self.nodes if report: - return {node: sqrt(distance) for node in nodes if (distance := distance_point_point_sqrd(node.xyz, point)) < d2} - else: - return [node for node in nodes if (distance := distance_point_point_sqrd(node.xyz, point)) < d2] + return {node: sqrt(distance) for node in nodes if distance_point_point_sqrd(node.xyz, point) < d2} + return [node for node in nodes if distance_point_point_sqrd(node.xyz, point) < d2] def find_closest_nodes_to_point(self, point, distance, number_of_nodes=1, plane=None): # type: (Point, float, int, Plane) -> list(Node) @@ -520,6 +525,7 @@ def find_nodes_around_node(self, node, distance, plane=None): ------- [:class:`compas_fea2.model.Node] The nodes around the given node + """ nodes = self.find_nodes_by_location(node.xyz, distance, plane, report=True) if node in nodes: @@ -555,10 +561,6 @@ def find_nodes_by_attribute(self, attr, value, tolerance=0.001): # type: (str, float, float) -> list(Node) """Find all nodes with a given value for a the given attribute. - Note - ---- - Only numeric attributes are supported. - Parameters ---------- attr : str @@ -570,6 +572,10 @@ def find_nodes_by_attribute(self, attr, value, tolerance=0.001): ------- list[:class:`compas_fea2.model.Node`] + Notes + ----- + Only numeric attributes are supported. + """ return list(filter(lambda x: abs(getattr(x, attr) - value) <= tolerance, self.nodes)) @@ -590,7 +596,6 @@ def find_nodes_on_plane(self, plane): return list(filter(lambda x: is_point_on_plane(Point(*x.xyz), plane), self.nodes)) def find_nodes_in_polygon(self, polygon, tolerance=1.1): - # type: (Polygon, float) -> list(Node) """Find the nodes of the part that are contained within a planar polygon Parameters @@ -602,22 +607,22 @@ def find_nodes_in_polygon(self, polygon, tolerance=1.1): ------- [:class:`compas_fea2.model.Node] List with the nodes contained in the polygon. + """ # TODO quick fix...change! - if not hasattr(polygon, 'plane'): + if not hasattr(polygon, "plane"): try: polygon.plane = Frame.from_points(*polygon.points[:3]) - except: + except Exception: polygon.plane = Frame.from_points(*polygon.points[-3:]) - S = Scale.from_factors([tolerance]*3, polygon.plane) + S = Scale.from_factors([tolerance] * 3, polygon.plane) T = Transformation.from_frame_to_frame(polygon.plane, Frame.worldXY()) nodes_on_plane = self.find_nodes_on_plane(Plane.from_frame(polygon.plane)) polygon_xy = polygon.transformed(S) polygon_xy = polygon.transformed(T) return list(filter(lambda x: is_point_in_polygon_xy(Point(*x.xyz).transformed(T), polygon_xy), nodes_on_plane)) - # TODO quite slow...check how to make it faster def find_nodes_where(self, conditions): # type: (list(str)) -> list(Node) @@ -632,8 +637,10 @@ def find_nodes_where(self, conditions): ------- [Node] List with the nodes matching the criteria. + """ import re + nodes = [] for condition in conditions: # limit the serch to the already found nodes @@ -642,7 +649,9 @@ def find_nodes_where(self, conditions): eval(condition) except NameError as ne: var_name = re.findall(r"'([^']*)'", str(ne))[0] - nodes.append(set(filter(lambda n: eval(condition.replace(var_name, str(getattr(n, var_name)))), part_nodes))) + nodes.append( + set(filter(lambda n: eval(condition.replace(var_name, str(getattr(n, var_name)))), part_nodes)) + ) return list(set.intersection(*nodes)) def contains_node(self, node): @@ -664,17 +673,13 @@ def add_node(self, node): # type: (Node) -> Node """Add a node to the part. - Note - ---- - By adding a Node to the part, it gets registered to the part. - Parameters ---------- node : :class:`compas_fea2.model.Node` The node. - Return - ------ + Returns + ------- :class:`compas_fea2.model.Node` The identifier of the node in the part. @@ -683,6 +688,10 @@ def add_node(self, node): TypeError If the node is not a node. + Notes + ----- + By adding a Node to the part, it gets registered to the part. + Examples -------- >>> part = DeformablePart() @@ -691,17 +700,17 @@ def add_node(self, node): """ if not isinstance(node, Node): - raise TypeError('{!r} is not a node.'.format(node)) + raise TypeError("{!r} is not a node.".format(node)) if self.contains_node(node): if compas_fea2.VERBOSE: - print('NODE SKIPPED: Node {!r} already in part.'.format(node)) + print("NODE SKIPPED: Node {!r} already in part.".format(node)) return if not compas_fea2.POINT_OVERLAP: if self.find_nodes_by_location(node.xyz, distance=compas_fea2.GLOBAL_TOLERANCE): if compas_fea2.VERBOSE: - print('NODE SKIPPED: Part {!r} has already a node at {}.'.format(self, node.xyz)) + print("NODE SKIPPED: Part {!r} has already a node at {}.".format(self, node.xyz)) return node._key = len(self._nodes) @@ -709,7 +718,7 @@ def add_node(self, node): self._gkey_node[node.gkey] = node node._registration = self if compas_fea2.VERBOSE: - print('Node {!r} registered to {!r}.'.format(node, self)) + print("Node {!r} registered to {!r}.".format(node, self)) return node def add_nodes(self, nodes): @@ -721,8 +730,8 @@ def add_nodes(self, nodes): nodes : list[:class:`compas_fea2.model.Node`] The list of nodes. - Return - ------ + Returns + ------- list[:class:`compas_fea2.model.Node`] The identifiers of the nodes in the part. @@ -740,14 +749,15 @@ def add_nodes(self, nodes): def remove_node(self, node): """Remove a :class:`compas_fea2.model.Node` from the part. - Warning - ------- + Warnings + -------- Removing nodes can cause inconsistencies. Parameters ---------- node : :class:`compas_fea2.model.Node` The node to remove + """ # type: (Node) -> None if self.contains_node(node): @@ -755,19 +765,20 @@ def remove_node(self, node): self._gkey_node.pop(node.gkey) node._registration = None if compas_fea2.VERBOSE: - print('Node {!r} removed from {!r}.'.format(node, self)) + print("Node {!r} removed from {!r}.".format(node, self)) def remove_nodes(self, nodes): """Remove multiple :class:`compas_fea2.model.Node` from the part. - Warning - ------- + Warnings + -------- Removing nodes can cause inconsistencies. Parameters ---------- - nodes : []:class:`compas_fea2.model.Node`] + nodes : [:class:`compas_fea2.model.Node`] List with the nodes to remove + """ for node in nodes: self.remove_node(node) @@ -783,28 +794,29 @@ def is_node_on_boundary(self, node, precision=None): precision : ?? ??? - Note - ---- - The `discretized_boundary_mesh` of the part must have been previously - defined. - Returns ------- bool `True` if the node is on the boundary, `False` otherwise. + + Notes + ----- + The `discretized_boundary_mesh` of the part must have been previously defined. + """ if not self.discretized_boundary_mesh: raise AttributeError("The discretized_boundary_mesh has not been defined") if not node.on_boundary: - node._on_boundary = True if geometric_key( - node.xyz, precision) in self.discretized_boundary_mesh.gkey_vertex() else False + node._on_boundary = ( + True if geometric_key(node.xyz, precision) in self.discretized_boundary_mesh.gkey_vertex() else False + ) return node.on_boundary # ========================================================================= # Elements methods # ========================================================================= def find_element_by_key(self, key): - # type: (int) -> _Element + # type: (int) -> Element """Retrieve an element in the model using its key. Parameters @@ -814,15 +826,16 @@ def find_element_by_key(self, key): Returns ------- - :class:`compas_fea2.model._Element` + :class:`compas_fea2.model.Element` The corresponding element. + """ for element in self.elements: if element.key == key: return element def find_elements_by_name(self, name): - # type: (str) -> list(_Element) + # type: (str) -> list(Element) """Find all elements with a given name. Parameters @@ -831,18 +844,18 @@ def find_elements_by_name(self, name): Returns ------- - list[:class:`compas_fea2.model._Element`] + list[:class:`compas_fea2.model.Element`] """ return [element for element in self.elements if element.name == name] def contains_element(self, element): - # type: (_Element) -> _Element + # type: (Element) -> Element """Verify that the part contains a specific element. Parameters ---------- - element : :class:`compas_fea2.model._Element` + element : :class:`compas_fea2.model.Element` Returns ------- @@ -852,17 +865,17 @@ def contains_element(self, element): return element in self.elements def add_element(self, element): - # type: (_Element) -> _Element + # type: (Element) -> Element """Add an element to the part. Parameters ---------- - element : :class:`compas_fea2.model._Element` + element : :class:`compas_fea2.model.Element` The element instance. Returns ------- - :class:`compas_fea2.model._Element` + :class:`compas_fea2.model.Element` Raises ------ @@ -870,8 +883,8 @@ def add_element(self, element): If the element is not an element. """ - if not isinstance(element, _Element): - raise TypeError('{!r} is not an element.'.format(element)) + if not isinstance(element, Element): + raise TypeError("{!r} is not an element.".format(element)) if self.contains_element(element): if compas_fea2.VERBOSE: @@ -879,11 +892,11 @@ def add_element(self, element): return self.add_nodes(element.nodes) - if hasattr(element, 'section'): + if hasattr(element, "section"): if element.section: self.add_section(element.section) - if hasattr(element.section, 'material'): + if hasattr(element.section, "material"): if element.section.material: self.add_material(element.section.material) @@ -891,54 +904,56 @@ def add_element(self, element): self.elements.add(element) element._registration = self if compas_fea2.VERBOSE: - print('Element {!r} registered to {!r}.'.format(element, self)) + print("Element {!r} registered to {!r}.".format(element, self)) return element def add_elements(self, elements): - # type: (_Element) -> list(_Element) + # type: (Element) -> list(Element) """Add multiple elements to the part. Parameters ---------- - elements : list[:class:`compas_fea2.model._Element`] + elements : list[:class:`compas_fea2.model.Element`] - Return - ------ - list[:class:`compas_fea2.model._Element`] + Returns + ------- + list[:class:`compas_fea2.model.Element`] """ return [self.add_element(element) for element in elements] def remove_element(self, element): - """Remove a :class:`compas_fea2.model._Element` from the part. - - Warning - ------- - Removing elements can cause inconsistencies. + """Remove a :class:`compas_fea2.model.Element` from the part. Parameters ---------- - element : :class:`compas_fea2.model._Element` + element : :class:`compas_fea2.model.Element` The element to remove + + Warnings + -------- + Removing elements can cause inconsistencies. + """ - # type: (_Element) -> None + # type: (Element) -> None if self.contains_node(element): self.elements.pop(element) element._registration = None if compas_fea2.VERBOSE: - print('Element {!r} removed from {!r}.'.format(element, self)) + print("Element {!r} removed from {!r}.".format(element, self)) def remove_elements(self, elements): - """Remove multiple :class:`compas_fea2.model._Element` from the part. - - Warning - ------- - Removing elements can cause inconsistencies. + """Remove multiple :class:`compas_fea2.model.Element` from the part. Parameters ---------- - elements : []:class:`compas_fea2.model._Element`] + elements : []:class:`compas_fea2.model.Element`] List with the elements to remove + + Warnings + -------- + Removing elements can cause inconsistencies. + """ for element in elements: self.remove_element(element) @@ -948,15 +963,16 @@ def is_element_on_boundary(self, element): Parameters ---------- - element : :class:`compas_fea2.model._Element` + element : :class:`compas_fea2.model.Element` The element to check. Returns ------- bool ``True`` if the element is on the boundary. + """ - # type: (_Element) -> bool + # type: (Element) -> bool from compas.geometry import centroid_points if element.on_boundary is None: @@ -964,13 +980,20 @@ def is_element_on_boundary(self, element): centroid_face = {} for face in self._discretized_boundary_mesh.faces(): centroid_face[geometric_key(self._discretized_boundary_mesh.face_centroid(face))] = face - if isinstance(element, _Element3D): - if any(geometric_key(centroid_points([node.xyz for node in face.nodes])) in self._discretized_boundary_mesh.centroid_face for face in element.faces): + if isinstance(element, Element3D): + if any( + geometric_key(centroid_points([node.xyz for node in face.nodes])) + in self._discretized_boundary_mesh.centroid_face + for face in element.faces + ): element.on_boundary = True else: element.on_boundary = False - elif isinstance(element, _Element2D): - if geometric_key(centroid_points([node.xyz for node in element.nodes])) in self._discretized_boundary_mesh.centroid_face: + elif isinstance(element, Element2D): + if ( + geometric_key(centroid_points([node.xyz for node in element.nodes])) + in self._discretized_boundary_mesh.centroid_face + ): element.on_boundary = True else: element.on_boundary = False @@ -981,13 +1004,8 @@ def is_element_on_boundary(self, element): # ========================================================================= def find_faces_on_plane(self, plane): - # type: (Plane) -> list(Face) """Find the face of the elements that belongs to a given plane, if any. - Note - ---- - The search is limited to solid elements. - Parameters ---------- plane : :class:`compas.geometry.Plane` @@ -997,9 +1015,16 @@ def find_faces_on_plane(self, plane): ------- [:class:`compas_fea2.model.Face`] list with the faces belonging to the given plane. + + Notes + ----- + The search is limited to solid elements. + """ faces = [] - for element in filter(lambda x: isinstance(x, (_Element2D, _Element3D)) and self.is_element_on_boundary(x), self._elements): + for element in filter( + lambda x: isinstance(x, (Element2D, Element3D)) and self.is_element_on_boundary(x), self._elements + ): for face in element.faces: if all([is_point_on_plane(node.xyz, plane) for node in face.nodes]): faces.append(face) @@ -1042,7 +1067,7 @@ def contains_group(self, group): elif isinstance(group, FacesGroup): return group in self._facesgroups else: - raise TypeError('{!r} is not a valid Group'.format(group)) + raise TypeError("{!r} is not a valid Group".format(group)) def add_group(self, group): """Add a node or element group to the part. @@ -1051,8 +1076,8 @@ def add_group(self, group): ---------- group : :class:`compas_fea2.model.NodeGroup` | :class:`compas_fea2.model.ElementGroup` - Return - ------ + Returns + ------- None Raises @@ -1089,8 +1114,8 @@ def add_groups(self, groups): ---------- groups : list[:class:`compas_fea2.model.Group`] - Return - ------ + Returns + ------- list[:class:`compas_fea2.model.Group`] """ @@ -1100,14 +1125,14 @@ def add_groups(self, groups): # Results methods # ============================================================================== - def sorted_nodes_by_displacement(self, problem, step=None, component='length'): + def sorted_nodes_by_displacement(self, problem, step=None, component="length"): """Return a list with the nodes sorted by their displacement Parameters ---------- problem : :class:`compas_fea2.problem.Problem` The problem - step : :class:`compas_fea2.problem._Step`, optional + step : :class:`compas_fea2.problem.Step`, optional The step, by default None. If not provided, the last step of the problem is used. component : str, optional @@ -1117,18 +1142,19 @@ def sorted_nodes_by_displacement(self, problem, step=None, component='length'): ------- [:class:`compas_fea2.model.Node`] The node sorted by displacment (ascending). + """ step = step or problem._steps_order[-1] - return sorted(self.nodes, key=lambda n: getattr(Vector(*n.results[problem][step].get('U', None)), component)) + return sorted(self.nodes, key=lambda n: getattr(Vector(*n.results[problem][step].get("U", None)), component)) - def get_max_displacement(self, problem, step=None, component='length'): + def get_max_displacement(self, problem, step=None, component="length"): """Retrieve the node with the maximum displacement Parameters ---------- problem : :class:`compas_fea2.problem.Problem` The problem - step : :class:`compas_fea2.problem._Step`, optional + step : :class:`compas_fea2.problem.Step`, optional The step, by default None. If not provided, the last step of the problem is used. component : str, optional @@ -1138,20 +1164,21 @@ def get_max_displacement(self, problem, step=None, component='length'): ------- :class:`compas_fea2.model.Node`, float The node and the displacement + """ step = step or problem._steps_order[-1] node = self.sorted_nodes_by_displacement(problem=problem, step=step, component=component)[-1] - displacement = getattr(Vector(*node.results[problem][step].get('U', None)), component) + displacement = getattr(Vector(*node.results[problem][step].get("U", None)), component) return node, displacement - def get_min_displacement(self, problem, step=None, component='length'): + def get_min_displacement(self, problem, step=None, component="length"): """Retrieve the node with the minimum displacement Parameters ---------- problem : :class:`compas_fea2.problem.Problem` The problem - step : :class:`compas_fea2.problem._Step`, optional + step : :class:`compas_fea2.problem.Step`, optional The step, by default None. If not provided, the last step of the problem is used. component : str, optional @@ -1161,20 +1188,21 @@ def get_min_displacement(self, problem, step=None, component='length'): ------- :class:`compas_fea2.model.Node`, float The node and the displacement + """ step = step or problem._steps_order[-1] node = self.sorted_nodes_by_displacement(problem=problem, step=step, component=component)[0] - displacement = getattr(Vector(*node.results[problem][step].get('U', None)), component) + displacement = getattr(Vector(*node.results[problem][step].get("U", None)), component) return node, displacement - def get_average_displacement_at_point(self, problem, point, distance, step=None, component='length', project=False): + def get_average_displacement_at_point(self, problem, point, distance, step=None, component="length", project=False): """Compute the average displacement around a point Parameters ---------- problem : :class:`compas_fea2.problem.Problem` The problem - step : :class:`compas_fea2.problem._Step`, optional + step : :class:`compas_fea2.problem.Step`, optional The step, by default None. If not provided, the last step of the problem is used. component : str, optional @@ -1184,27 +1212,27 @@ def get_average_displacement_at_point(self, problem, point, distance, step=None, ------- :class:`compas_fea2.model.Node`, float The node and the displacement + """ step = step or problem._steps_order[-1] nodes = self.find_nodes_by_location(point=point, distance=distance, report=True) if nodes: - displacements = [getattr(Vector(*node.results[problem][step].get('U', None)), component) for node in nodes] - return point, sum(displacements)/len(displacements) + displacements = [getattr(Vector(*node.results[problem][step].get("U", None)), component) for node in nodes] + return point, sum(displacements) / len(displacements) -class DeformablePart(_Part): +class DeformablePart(Part): """Deformable part. - """ - __doc__ += _Part.__doc__ - __doc__ += """ - Additional Attributes - --------------------- - materials : Set[:class:`compas_fea2.model._Material`] + + Attributes + ---------- + materials : Set[:class:`compas_fea2.model.Material`] The materials belonging to the part. - sections : Set[:class:`compas_fea2.model._Section`] + sections : Set[:class:`compas_fea2.model.Section`] The sections belonging to the part. - releases : Set[:class:`compas_fea2.model._BeamEndRelease`] + releases : Set[:class:`compas_fea2.model.BeamEndRelease`] The releases belonging to the part. + """ def __init__(self, name=None, **kwargs): @@ -1213,15 +1241,15 @@ def __init__(self, name=None, **kwargs): self._sections = set() self._releases = set() - @ property + @property def materials(self): return self._materials - @ property + @property def sections(self): return self._sections - @ property + @property def releases(self): return self._releases @@ -1229,10 +1257,11 @@ def releases(self): # Constructor methods # ========================================================================= - @ classmethod - @ timer(message='compas Mesh successfully imported in ') + @classmethod + @timer(message="compas Mesh successfully imported in ") def frame_from_compas_mesh(cls, mesh, section, name=None, **kwargs): """Creates a DeformablePart object from a a :class:`compas.datastructures.Mesh`. + To each edge of the mesh is assigned a :class:`compas_fea2.model.BeamElement`. Currently, the same section is applied to all the elements. @@ -1251,7 +1280,7 @@ def frame_from_compas_mesh(cls, mesh, section, name=None, **kwargs): for edge in mesh.edges(): nodes = [vertex_node[vertex] for vertex in edge] - v = mesh.edge_direction(*edge) + v = list(mesh.edge_direction(edge)) v.append(v.pop(0)) part.add_element(BeamElement(nodes=[*nodes], section=section, frame=v)) @@ -1260,17 +1289,15 @@ def frame_from_compas_mesh(cls, mesh, section, name=None, **kwargs): return part - @ classmethod + @classmethod # @timer(message='part successfully imported from gmsh model in ') def from_gmsh(cls, gmshModel, section, name=None, **kwargs): - """ - """ + """ """ return super().from_gmsh(gmshModel, name=name, section=section, **kwargs) - @ classmethod + @classmethod def from_boundary_mesh(cls, boundary_mesh, section, name=None, **kwargs): - """ - """ + """ """ return super().from_boundary_mesh(boundary_mesh, section=section, name=name, **kwargs) # ========================================================================= @@ -1278,7 +1305,7 @@ def from_boundary_mesh(cls, boundary_mesh, section, name=None, **kwargs): # ========================================================================= def find_materials_by_name(self, name): - # type: (str) -> list(_Material) + # type: (str) -> list(Material) """Find all materials with a given name. Parameters @@ -1293,7 +1320,7 @@ def find_materials_by_name(self, name): return [material for material in self.materials if material.name == name] def contains_material(self, material): - # type: (_Material) -> _Material + # type: (Material) -> Material """Verify that the part contains a specific material. Parameters @@ -1308,7 +1335,7 @@ def contains_material(self, material): return material in self.materials def add_material(self, material): - # type: (_Material) -> _Material + # type: (Material) -> Material """Add a material to the part so that it can be referenced in section and element definitions. Parameters @@ -1325,12 +1352,12 @@ def add_material(self, material): If the material is not a material. """ - if not isinstance(material, _Material): - raise TypeError('{!r} is not a material.'.format(material)) + if not isinstance(material, Material): + raise TypeError("{!r} is not a material.".format(material)) if self.contains_material(material): if compas_fea2.VERBOSE: - print('SKIPPED: Material {!r} already in part.'.format(material)) + print("SKIPPED: Material {!r} already in part.".format(material)) return material._key = len(self._materials) @@ -1339,7 +1366,7 @@ def add_material(self, material): return material def add_materials(self, materials): - # type: (_Material) -> list(_Material) + # type: (Material) -> list(Material) """Add multiple materials to the part. Parameters @@ -1358,7 +1385,7 @@ def add_materials(self, materials): # ========================================================================= def find_sections_by_name(self, name): - # type: (str) -> list(_Section) + # type: (str) -> list(Section) """Find all sections with a given name. Parameters @@ -1373,7 +1400,7 @@ def find_sections_by_name(self, name): return [section for section in self.sections if section.name == name] def contains_section(self, section): - # type: (_Section) -> _Section + # type: (Section) -> Section """Verify that the part contains a specific section. Parameters @@ -1388,7 +1415,7 @@ def contains_section(self, section): return section in self.sections def add_section(self, section): - # type: (_Section) -> _Section + # type: (Section) -> Section """Add a section to the part so that it can be referenced in element definitions. Parameters @@ -1405,8 +1432,8 @@ def add_section(self, section): If the section is not a section. """ - if not isinstance(section, _Section): - raise TypeError('{!r} is not a section.'.format(section)) + if not isinstance(section, Section): + raise TypeError("{!r} is not a section.".format(section)) if self.contains_section(section): if compas_fea2.VERBOSE: @@ -1420,7 +1447,7 @@ def add_section(self, section): return section def add_sections(self, sections): - # type: (list(_Section)) -> _Section + # type: (list(Section)) -> Section """Add multiple sections to the part. Parameters @@ -1439,8 +1466,7 @@ def add_sections(self, sections): # ========================================================================= def add_beam_release(self, element, location, release): - """Add a :class:`compas_fea2.model._BeamEndRelease` to an element in the - part. + """Add a :class:`compas_fea2.model.BeamEndRelease` to an element in the part. Parameters ---------- @@ -1448,73 +1474,72 @@ def add_beam_release(self, element, location, release): The element to release. location : str 'start' or 'end'. - release : :class:`compas_fea2.model._BeamEndRelease` + release : :class:`compas_fea2.model.BeamEndRelease` Release type to apply. + """ - if not isinstance(release, _BeamEndRelease): - raise TypeError('{!r} is not a beam release element.'.format(release)) + if not isinstance(release, BeamEndRelease): + raise TypeError("{!r} is not a beam release element.".format(release)) release.element = element release.location = location self._releases.add(release) return release -class RigidPart(_Part): +class RigidPart(Part): """Rigid part. - """ - __doc__ += _Part.__doc__ - __doc__ += """ - Addtional Attributes - -------------------- + + Attributes + ---------- reference_point : :class:`compas_fea2.model.Node` A node acting as a reference point for the part, by default `None`. This is required if the part is rigid as it controls its movement in space. + """ def __init__(self, reference_point=None, name=None, **kwargs): super(RigidPart, self).__init__(name=name, **kwargs) self._reference_point = reference_point - @ property + @property def reference_point(self): return self._reference_point - @ reference_point.setter + @reference_point.setter def reference_point(self, value): self._reference_point = self.add_node(value) value._is_reference = True - @ classmethod + @classmethod # @timer(message='part successfully imported from gmsh model in ') def from_gmsh(cls, gmshModel, name=None, **kwargs): - """ - """ - kwargs['rigid'] = True + """ """ + kwargs["rigid"] = True return super().from_gmsh(gmshModel, name=name, **kwargs) - @ classmethod + @classmethod def from_boundary_mesh(cls, boundary_mesh, name=None, **kwargs): - """ - """ - kwargs['rigid'] = True + """ """ + kwargs["rigid"] = True return super().from_boundary_mesh(boundary_mesh, name=name, **kwargs) + # ========================================================================= # Elements methods # ========================================================================= # TODO this can be removed and the checks on the rigid part can be done in _part def add_element(self, element): - # type: (_Element) -> _Element + # type: (Element) -> Element """Add an element to the part. Parameters ---------- - element : :class:`compas_fea2.model._Element` + element : :class:`compas_fea2.model.Element` The element instance. Returns ------- - :class:`compas_fea2.model._Element` + :class:`compas_fea2.model.Element` Raises ------ @@ -1522,8 +1547,8 @@ def add_element(self, element): If the element is not an element. """ - if not hasattr(element, 'rigid'): - raise TypeError('The element type cannot be assigned to a RigidPart') - if not getattr(element, 'rigid'): - raise TypeError('Rigid parts can only have rigid elements') + if not hasattr(element, "rigid"): + raise TypeError("The element type cannot be assigned to a RigidPart") + if not getattr(element, "rigid"): + raise TypeError("Rigid parts can only have rigid elements") return super().add_element(element) diff --git a/src/compas_fea2/model/releases.py b/src/compas_fea2/model/releases.py index 317e2715d..e1b0bfe17 100644 --- a/src/compas_fea2/model/releases.py +++ b/src/compas_fea2/model/releases.py @@ -3,18 +3,14 @@ from __future__ import print_function from compas_fea2.base import FEAData -from compas.geometry import Frame import compas_fea2.model -class _BeamEndRelease(FEAData): +class BeamEndRelease(FEAData): """Assign a general end release to a `compas_fea2.model.BeamElement`. Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. n : bool, optional Release displacements along the local axial direction, by default False v1 : bool, optional @@ -30,9 +26,6 @@ class _BeamEndRelease(FEAData): Attributes ---------- - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. location : str 'start' or 'end' element : :class:`compas_fea2.model.BeamElement` @@ -52,8 +45,8 @@ class _BeamEndRelease(FEAData): """ - def __init__(self, n=False, v1=False, v2=False, m1=False, m2=False, t=False, name=None, **kwargs): - super(_BeamEndRelease, self).__init__(name, **kwargs) + def __init__(self, n=False, v1=False, v2=False, m1=False, m2=False, t=False, **kwargs): + super(BeamEndRelease, self).__init__(**kwargs) self._element = None self._location = None @@ -71,7 +64,7 @@ def element(self): @element.setter def element(self, value): if not isinstance(value, compas_fea2.model.BeamElement): - raise TypeError('{!r} is not a beam element.'.format(value)) + raise TypeError("{!r} is not a beam element.".format(value)) self._element = value @property @@ -80,12 +73,12 @@ def location(self): @location.setter def location(self, value): - if not value in ('start', 'end'): - raise TypeError('the location can be either `start` or `end`') + if value not in ("start", "end"): + raise TypeError("the location can be either `start` or `end`") self._location = value -class BeamEndPinRelease(_BeamEndRelease): +class BeamEndPinRelease(BeamEndRelease): """Assign a pin end release to a `compas_fea2.model.BeamElement`. Parameters @@ -96,13 +89,14 @@ class BeamEndPinRelease(_BeamEndRelease): Release rotations about local 2 direction, by default False t : bool, optional Release rotations about local axial direction (torsion), by default False + """ - def __init__(self, m1=False, m2=False, t=False, name=None, **kwargs): - super(BeamEndPinRelease, self).__init__(n=False, v1=False, v2=False, m1=m1, m2=m2, t=t, name=name, **kwargs) + def __init__(self, m1=False, m2=False, t=False, **kwargs): + super(BeamEndPinRelease, self).__init__(n=False, v1=False, v2=False, m1=m1, m2=m2, t=t, **kwargs) -class BeamEndSliderRelease(_BeamEndRelease): +class BeamEndSliderRelease(BeamEndRelease): """Assign a slider end release to a `compas_fea2.model.BeamElement`. Parameters @@ -111,8 +105,8 @@ class BeamEndSliderRelease(_BeamEndRelease): Release displacements along local 1 direction, by default False v2 : bool, optional Release displacements along local 2 direction, by default False + """ - def __init__(self, v1=False, v2=False, name=None, **kwargs): - super(BeamEndSliderRelease, self).__init__(v1=v1, v2=v2, - n=False, m1=False, m2=False, t=False, name=name, **kwargs) + def __init__(self, v1=False, v2=False, **kwargs): + super(BeamEndSliderRelease, self).__init__(v1=v1, v2=v2, n=False, m1=False, m2=False, t=False, **kwargs) diff --git a/src/compas_fea2/model/sections.py b/src/compas_fea2/model/sections.py index 82c6bb84e..33e37f987 100644 --- a/src/compas_fea2/model/sections.py +++ b/src/compas_fea2/model/sections.py @@ -2,45 +2,39 @@ from __future__ import division from __future__ import print_function -from abc import abstractmethod from math import pi from compas_fea2 import units from compas_fea2.base import FEAData -from .materials import _Material +from .materials import Material -class _Section(FEAData): +class Section(FEAData): """Base class for sections. - Note - ---- - Sections are registered to a :class:`compas_fea2.model.Model` and can be assigned - to elements in different Parts. - Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. - material : :class:`~compas_fea2.model._Material` + material : :class:`~compas_fea2.model.Material` A material definition. Attributes ---------- - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. key : int, read-only Identifier index of the section in the parent Model. - material : :class:`~compas_fea2.model._Material` + material : :class:`~compas_fea2.model.Material` The material associated with the section. model : :class:`compas_fea2.model.Model` The model where the section is assigned. + + Notes + ----- + Sections are registered to a :class:`compas_fea2.model.Model` and can be assigned + to elements in different Parts. + """ - def __init__(self, material, name=None, **kwargs): - super(_Section, self).__init__(name=name, **kwargs) + def __init__(self, material, **kwargs): + super(Section, self).__init__(**kwargs) self._key = None self._material = material @@ -55,8 +49,8 @@ def material(self): @material.setter def material(self, value): if value: - if not isinstance(value, _Material): - raise ValueError('Material must be of type `compas_fea2.model._Material`.') + if not isinstance(value, Material): + raise ValueError("Material must be of type `compas_fea2.model.Material`.") self._material = value @property @@ -70,36 +64,35 @@ def __str__(self): model : {!r} key : {} material : {!r} -""".format(self.name, '-'*len(self.name), self.model, self.key, self.material) +""".format( + self.name, "-" * len(self.name), self.model, self.key, self.material + ) # ============================================================================== # 0D # ============================================================================== + class MassSection(FEAData): """Section for point mass elements. Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. mass : float Point mass value. Attributes ---------- - name : str - Uniqe identifier. key : int, read-only Identifier of the element in the parent part. mass : float Point mass value. + """ - def __init__(self, mass, name=None, **kwargs): - super(MassSection, self).__init__(name=name, **kwargs) + def __init__(self, mass, **kwargs): + super(MassSection, self).__init__(**kwargs) self.mass = mass self._key = None @@ -113,7 +106,9 @@ def __str__(self): --------{} model : {!r} mass : {} -""".format(self.name, '-'*len(self.name), self.model, self.mass) +""".format( + self.name, "-" * len(self.name), self.model, self.mass + ) class SpringSection(FEAData): @@ -121,9 +116,6 @@ class SpringSection(FEAData): Parameters ---------- - name : str, optional - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. forces : dict Forces data for non-linear springs. displacements : dict @@ -133,8 +125,6 @@ class SpringSection(FEAData): Attributes ---------- - name : str - Uniqe identifier. key : int, read-only Identifier of the element in the parent part. forces : dict @@ -144,17 +134,17 @@ class SpringSection(FEAData): stiffness : dict Elastic stiffness for linear springs. - Note - ---- + Notes + ----- - Force and displacement data should range from negative to positive values. - Requires either a stiffness dict for linear springs, or forces and displacement lists for non-linear springs. - Directions are 'axial', 'lateral', 'rotation'. """ - def __init__(self, forces=None, displacements=None, stiffness=None, name=None, **kwargs): - super(SpringSection, self).__init__(name=name, **kwargs) - #TODO would be good to know the structure of these dicts and validate + def __init__(self, forces=None, displacements=None, stiffness=None, **kwargs): + super(SpringSection, self).__init__(**kwargs) + # TODO would be good to know the structure of these dicts and validate self.forces = forces or {} self.displacements = displacements or {} self.stiffness = stiffness or {} @@ -168,14 +158,17 @@ def __str__(self): forces : {} displ : {} stiffness : {} -""".format(self.name, self.forces, self.displacements, self.stiffness) +""".format( + self.name, self.forces, self.displacements, self.stiffness + ) # ============================================================================== # 1D # ============================================================================== -class BeamSection(_Section): + +class BeamSection(Section): """Custom section for beam elements. Parameters @@ -198,11 +191,8 @@ class BeamSection(_Section): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -224,16 +214,13 @@ class BeamSection(_Section): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, *, A, Ixx, Iyy, Ixy, Avx, Avy, J, g0, gw, material, name=None, **kwargs): - super(BeamSection, self).__init__(material=material, name=name, **kwargs) + def __init__(self, *, A, Ixx, Iyy, Ixy, Avx, Avy, J, g0, gw, material, **kwargs): + super(BeamSection, self).__init__(material=material, **kwargs) self.A = A self.Ixx = Ixx self.Iyy = Iyy @@ -260,28 +247,26 @@ def __str__(self): J : {} g0 : {} gw : {} -""".format(self.__class__.__name__, - len(self.__class__.__name__) * '-', - self.name, - self.material, - (self.A * units['m**2']), - (self.Ixx * units['m**4']), - (self.Iyy * units['m**4']), - (self.Ixy * units['m**4']), - (self.Avx * units['m**2']), - (self.Avy * units['m**2']), - self.J, - self.g0, - self.gw) +""".format( + self.__class__.__name__, + len(self.__class__.__name__) * "-", + self.name, + self.material, + (self.A * units["m**2"]), + (self.Ixx * units["m**4"]), + (self.Iyy * units["m**4"]), + (self.Ixy * units["m**4"]), + (self.Avx * units["m**2"]), + (self.Avy * units["m**2"]), + self.J, + self.g0, + self.gw, + ) class AngleSection(BeamSection): """Uniform thickness angle cross-section for beam elements. - Warning - ------- - - Ixy not yet calculated. - Parameters ---------- w : float @@ -290,7 +275,7 @@ class AngleSection(BeamSection): Height. t : float Thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. name : str, optional Section name. If not provided, a unique identifier is automatically @@ -322,49 +307,56 @@ class AngleSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. name : str Section name. If not provided, a unique identifier is automatically assigned. + Warnings + -------- + - Ixy not yet calculated. + """ - def __init__(self, w, h, t, material, name=None, **kwargs): + def __init__(self, w, h, t, material, **kwargs): self.w = w self.h = h self.t = t - p = 2. * (w + h - t) + p = 2.0 * (w + h - t) xc = (w**2 + h * t - t**2) / p yc = (h**2 + w * t - t**2) / p A = t * (w + h - t) - Ixx = (1. / 3) * (w * h**3 - (w - t) * (h - t)**3) - self.A * (h - yc)**2 - Iyy = (1. / 3) * (h * w**3 - (h - t) * (w - t)**3) - self.A * (w - xc)**2 + Ixx = (1.0 / 3) * (w * h**3 - (w - t) * (h - t) ** 3) - self.A * (h - yc) ** 2 + Iyy = (1.0 / 3) * (h * w**3 - (h - t) * (w - t) ** 3) - self.A * (w - xc) ** 2 Ixy = 0 - J = (1. / 3) * (h + w - t) * t**3 + J = (1.0 / 3) * (h + w - t) * t**3 Avx = 0 Avy = 0 g0 = 0 gw = 0 - super(AngleSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(AngleSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) # TODO implement different thickness along the 4 sides class BoxSection(BeamSection): """Hollow rectangular box cross-section for beam elements. - Note - ---- - Currently you can only specify the thickness of the flanges and the webs. - - Warning - ------- - - Ixy not yet calculated. - Parameters ---------- w : float @@ -375,11 +367,8 @@ class BoxSection(BeamSection): Web thickness. tf : float Flange thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -409,15 +398,20 @@ class BoxSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + + Notes + ----- + Currently you can only specify the thickness of the flanges and the webs. + + Warnings + -------- + - Ixy not yet calculated. """ - def __init__(self, w, h, tw, tf, material, name=None, **kwargs): + def __init__(self, w, h, tw, tf, material, **kwargs): self.w = w self.h = h self.tw = tw @@ -427,8 +421,8 @@ def __init__(self, w, h, tw, tf, material, name=None, **kwargs): p = 2 * ((h - tf) / tw + (w - tw) / tf) A = w * h - (w - 2 * tw) * (h - 2 * tf) - Ixx = (w * h**3) / 12. - ((w - 2 * tw) * (h - 2 * tf)**3) / 12. - Iyy = (h * w**3) / 12. - ((h - 2 * tf) * (w - 2 * tw)**3) / 12. + Ixx = (w * h**3) / 12.0 - ((w - 2 * tw) * (h - 2 * tf) ** 3) / 12.0 + Iyy = (h * w**3) / 12.0 - ((h - 2 * tf) * (w - 2 * tw) ** 3) / 12.0 Ixy = 0 Avx = 0 Avy = 0 @@ -436,8 +430,19 @@ def __init__(self, w, h, tw, tf, material, name=None, **kwargs): g0 = 0 gw = 0 - super(BoxSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(BoxSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class CircularSection(BeamSection): @@ -447,11 +452,8 @@ class CircularSection(BeamSection): ---------- r : float Radius. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -475,19 +477,17 @@ class CircularSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + """ - def __init__(self, r, material, name=None, **kwargs): + def __init__(self, r, material, **kwargs): self.r = r D = 2 * r A = 0.25 * pi * D**2 - Ixx = Iyy = (pi * D**4) / 64. + Ixx = Iyy = (pi * D**4) / 64.0 Ixy = 0 Avx = 0 Avy = 0 @@ -495,8 +495,19 @@ def __init__(self, r, material, name=None, **kwargs): g0 = 0 gw = 0 - super(CircularSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(CircularSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class HexSection(BeamSection): @@ -535,24 +546,18 @@ class HexSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + """ - def __init__(self, r, t, material, name=None, **kwargs): - raise NotImplementedError('This section is not available for the selected backend') + def __init__(self, r, t, material, **kwargs): + raise NotImplementedError("This section is not available for the selected backend") class ISection(BeamSection): """Equal flanged I-section for beam elements. - Note - ---- - Currently you the thickness of the two flanges is the same. - Parameters ---------- w : float @@ -563,11 +568,8 @@ class ISection(BeamSection): Web thickness. tf : float Flange thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -597,31 +599,44 @@ class ISection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + + Notes + ----- + Currently you the thickness of the two flanges is the same. + """ - def __init__(self, w, h, tw, tf, material, name=None, **kwargs): + def __init__(self, w, h, tw, tf, material, **kwargs): self.w = w self.h = h self.tw = tw self.tf = tf A = 2 * w * tf + (h - 2 * tf) * tw - Ixx = (tw * (h - 2 * tf)**3) / 12. + 2 * ((tf**3) * w / 12. + w * tf * (h / 2. - tf / 2.)**2) - Iyy = ((h - 2 * tf) * tw**3) / 12. + 2 * ((w**3) * tf / 12.) + Ixx = (tw * (h - 2 * tf) ** 3) / 12.0 + 2 * ((tf**3) * w / 12.0 + w * tf * (h / 2.0 - tf / 2.0) ** 2) + Iyy = ((h - 2 * tf) * tw**3) / 12.0 + 2 * ((w**3) * tf / 12.0) Ixy = 0 Avx = 0 Avy = 0 - J = (1. / 3) * (2 * w * tf**3 + (h - tf) * tw**3) + J = (1.0 / 3) * (2 * w * tf**3 + (h - tf) * tw**3) g0 = 0 gw = 0 - super(ISection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(ISection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class PipeSection(BeamSection): @@ -633,11 +648,8 @@ class PipeSection(BeamSection): Outer radius. t : float Wall thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -663,30 +675,39 @@ class PipeSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + """ - def __init__(self, r, t, material, name=None, **kwargs): + def __init__(self, r, t, material, **kwargs): self.r = r self.t = t D = 2 * r - A = 0.25 * pi * (D**2 - (D - 2 * t)**2) - Ixx = Iyy = 0.25 * pi * (r**4 - (r - t)**4) + A = 0.25 * pi * (D**2 - (D - 2 * t) ** 2) + Ixx = Iyy = 0.25 * pi * (r**4 - (r - t) ** 4) Ixy = 0 Avx = 0 Avy = 0 - J = (2. / 3) * pi * (r + 0.5 * t) * t**3 + J = (2.0 / 3) * pi * (r + 0.5 * t) * t**3 g0 = 0 gw = 0 - super(PipeSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(PipeSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class RectangularSection(BeamSection): @@ -698,11 +719,8 @@ class RectangularSection(BeamSection): Width. h : float Height. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -728,15 +746,12 @@ class RectangularSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, w, h, material, name=None, **kwargs): + def __init__(self, w, h, material, **kwargs): self.w = w self.h = h @@ -744,8 +759,8 @@ def __init__(self, w, h, material, name=None, **kwargs): l2 = min([w, h]) A = w * h - Ixx = (1 / 12.) * w * h**3 - Iyy = (1 / 12.) * h * w**3 + Ixx = (1 / 12.0) * w * h**3 + Iyy = (1 / 12.0) * h * w**3 Ixy = 0 Avy = 0.833 * A Avx = 0.833 * A @@ -753,17 +768,24 @@ def __init__(self, w, h, material, name=None, **kwargs): g0 = 0 gw = 0 - super(RectangularSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(RectangularSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class TrapezoidalSection(BeamSection): """Solid trapezoidal cross-section for beam elements. - Warning - ------- - - J not yet calculated. - Parameters ---------- w1 : float @@ -772,11 +794,8 @@ class TrapezoidalSection(BeamSection): Width at top. h : float Height. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -804,15 +823,16 @@ class TrapezoidalSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + + Warnings + -------- + - J not yet calculated. """ - def __init__(self, w1, w2, h, material, name=None, **kwargs): + def __init__(self, w1, w2, h, material, **kwargs): self.w1 = w1 self.w2 = w2 self.h = h @@ -820,8 +840,8 @@ def __init__(self, w1, w2, h, material, name=None, **kwargs): # c = (h * (2 * w2 + w1)) / (3. * (w1 + w2)) # NOTE: not used A = 0.5 * (w1 + w2) * h - Ixx = (1 / 12.) * (3 * w2 + w1) * h**3 - Iyy = (1 / 48.) * h * (w1 + w2) * (w2**2 + 7 * w1**2) + Ixx = (1 / 12.0) * (3 * w2 + w1) * h**3 + Iyy = (1 / 48.0) * h * (w1 + w2) * (w2**2 + 7 * w1**2) Ixy = 0 Avx = 0 Avy = 0 @@ -829,8 +849,19 @@ def __init__(self, w1, w2, h, material, name=None, **kwargs): g0 = 0 gw = 0 - super(TrapezoidalSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(TrapezoidalSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class TrussSection(BeamSection): @@ -840,11 +871,8 @@ class TrussSection(BeamSection): ---------- A : float Area. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -866,15 +894,12 @@ class TrussSection(BeamSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, A, material, name=None, **kwargs): + def __init__(self, A, material, **kwargs): Ixx = 0 Iyy = 0 Ixy = 0 @@ -883,8 +908,19 @@ def __init__(self, A, material, name=None, **kwargs): J = 0 g0 = 0 gw = 0 - super(TrussSection, self).__init__(A=A, Ixx=Ixx, Iyy=Iyy, Ixy=Ixy, - Avx=Avx, Avy=Avy, J=J, g0=g0, gw=gw, material=material, name=name, **kwargs) + super(TrussSection, self).__init__( + A=A, + Ixx=Ixx, + Iyy=Iyy, + Ixy=Ixy, + Avx=Avx, + Avy=Avy, + J=J, + g0=g0, + gw=gw, + material=material, + **kwargs, + ) class StrutSection(TrussSection): @@ -894,11 +930,8 @@ class StrutSection(TrussSection): ---------- A : float Area. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -920,16 +953,13 @@ class StrutSection(TrussSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, A, material, name=None, **kwargs): - super(StrutSection, self).__init__(A=A, material=material, name=name, **kwargs) + def __init__(self, A, material, **kwargs): + super(StrutSection, self).__init__(A=A, material=material, **kwargs) class TieSection(TrussSection): @@ -939,11 +969,8 @@ class TieSection(TrussSection): ---------- A : float Area. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- @@ -965,78 +992,65 @@ class TieSection(TrussSection): ??? gw : float ??? - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + """ - def __init__(self, A, material, name=None, **kwargs): - super(TieSection, self).__init__(A=A, material=material, name=name, **kwargs) + def __init__(self, A, material, **kwargs): + super(TieSection, self).__init__(A=A, material=material, **kwargs) # ============================================================================== # 2D # ============================================================================== -class ShellSection(_Section): + +class ShellSection(Section): """Section for shell elements. Parameters ---------- t : float Thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- t : float Thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, t, material, name=None, **kwargs): - super(ShellSection, self).__init__(material=material, name=name, **kwargs) + def __init__(self, t, material, **kwargs): + super(ShellSection, self).__init__(material=material, **kwargs) self.t = t -class MembraneSection(_Section): +class MembraneSection(Section): """Section for membrane elements. Parameters ---------- t : float Thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- t : float Thickness. - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. """ - def __init__(self, t, material, name=None, **kwargs): - super(MembraneSection, self).__init__(material=material, name=name, **kwargs) + def __init__(self, t, material, **kwargs): + super(MembraneSection, self).__init__(material=material, **kwargs) self.t = t @@ -1044,25 +1058,21 @@ def __init__(self, t, material, name=None, **kwargs): # 3D # ============================================================================== -class SolidSection(_Section): + +class SolidSection(Section): """Section for solid elements. Parameters ---------- - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str, optional - Section name. If not provided, a unique identifier is automatically - assigned. Attributes ---------- - material : :class:`compas_fea2.model._Material` + material : :class:`compas_fea2.model.Material` The section material. - name : str - Section name. If not provided, a unique identifier is automatically - assigned. + """ - def __init__(self, material, name=None, **kwargs): - super(SolidSection, self).__init__(material=material, name=name, **kwargs) + def __init__(self, material, **kwargs): + super(SolidSection, self).__init__(material=material, **kwargs) diff --git a/src/compas_fea2/postprocess/__init__.py b/src/compas_fea2/postprocess/__init__.py index c6a1fea46..eb8d2f3ee 100644 --- a/src/compas_fea2/postprocess/__init__.py +++ b/src/compas_fea2/postprocess/__init__.py @@ -1,19 +1,3 @@ -""" -******************************************************************************** -postprocess -******************************************************************************** - -.. currentmodule:: compas_fea2.postprocess - -Stresses -======== - -.. autosummary:: - :toctree: generated/ - - principal_stresses - -""" from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -22,5 +6,5 @@ __all__ = [ - 'principal_stresses', + "principal_stresses", ] diff --git a/src/compas_fea2/postprocess/stresses.py b/src/compas_fea2/postprocess/stresses.py index 64017da9d..1d576c374 100644 --- a/src/compas_fea2/postprocess/stresses.py +++ b/src/compas_fea2/postprocess/stresses.py @@ -6,7 +6,7 @@ def principal_stresses(data): - """ Performs principal stress calculations solving the eigenvalues problem. + """Performs principal stress calculations solving the eigenvalues problem. Parameters ---------- @@ -28,10 +28,11 @@ def principal_stresses(data): Warnings -------- The function is experimental and works only for shell elements at the moment. + """ - components = ['sxx', 'sxy', 'syy'] - stype = ['max', 'min'] - section_points = ['sp1', 'sp5'] + components = ["sxx", "sxy", "syy"] + stype = ["max", "min"] + section_points = ["sp1", "sp5"] stress_results = list(zip(*[data[stress_name].values() for stress_name in components])) array_size = ((len(stress_results)), (2, len(stress_results))) @@ -42,8 +43,7 @@ def principal_stresses(data): # Stresses are computed as mean of the values at each integration points stress_vector = [np.mean(np.array([v for k, v in i.items() if sp in k])) for i in element_stresses] # The principal stresses and their directions are computed solving the eigenvalues problem - stress_matrix = np.array([(stress_vector[0], stress_vector[1]), - (stress_vector[1], stress_vector[2])]) + stress_matrix = np.array([(stress_vector[0], stress_vector[1]), (stress_vector[1], stress_vector[2])]) w_sp, v_sp = np.linalg.eig(stress_matrix) # sort by larger to smaller eigenvalue idx = w_sp.argsort()[::-1] diff --git a/src/compas_fea2/problem/__init__.py b/src/compas_fea2/problem/__init__.py index e0320f0c1..0906070c6 100644 --- a/src/compas_fea2/problem/__init__.py +++ b/src/compas_fea2/problem/__init__.py @@ -1,86 +1,3 @@ -""" -******************************************************************************** -problem -******************************************************************************** - -.. currentmodule:: compas_fea2.problem - -Problem -======= - -.. autosummary:: - :toctree: generated/ - - Problem - -Steps -===== - -.. autosummary:: - :toctree: generated/ - - _Step - _GeneralStep - _Perturbation - ModalAnalysis - ComplexEigenValue - StaticStep - LinearStaticPerturbation - BucklingAnalysis - DynamicStep - QuasiStaticStep - DirectCyclicStep - -Prescribed Fields -================= - -.. autosummary:: - :toctree: generated/ - - _PrescribedField - PrescribedTemperatureField - -Loads -===== - -.. autosummary:: - :toctree: generated/ - - _Load - PrestressLoad - PointLoad - LineLoad - AreaLoad - GravityLoad - TributaryLoad - HarmonicPointLoad - HarmonicPressureLoad - ThermalLoad - -Displacements -============= - -.. autosummary:: - :toctree: generated/ - - GeneralDisplacement - -Load Patterns -============= -.. autosummary:: - :toctree: generated/ - - Pattern - -Outputs -======= - -.. autosummary:: - :toctree: generated/ - - FieldOutput - HistoryOutput -""" from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -88,7 +5,7 @@ from .problem import Problem from .displacements import GeneralDisplacement from .loads import ( - _Load, + Load, PrestressLoad, PointLoad, LineLoad, @@ -99,67 +16,46 @@ HarmonicPressureLoad, ThermalLoad, ) -from .fields import ( - _PrescribedField, - PrescribedTemperatureField, -) - -from .patterns import ( - Pattern, -) -from .steps import ( - _Step, - _GeneralStep, - _Perturbation, - ModalAnalysis, - ComplexEigenValue, - StaticStep, - LinearStaticPerturbation, - BucklingAnalysis, - DynamicStep, - QuasiStaticStep, - DirectCyclicStep, -) +from .fields import PrescribedField, PrescribedTemperatureField +from .patterns import Pattern +from .steps.step import Step, GeneralStep +from .steps.dynamic import DynamicStep +from .steps.perturbations import Perturbation, ModalAnalysis, ComplexEigenValue, BucklingAnalysis +from .steps.quasistatic import QuasiStaticStep, DirectCyclicStep +from .steps.static import StaticStep -from .outputs import ( - FieldOutput, - HistoryOutput -) +from .outputs import FieldOutput, HistoryOutput __all__ = [ - 'Problem', - - 'GeneralDisplacement', - - '_Load', - 'PrestressLoad', - 'PointLoad', - 'LineLoad', - 'AreaLoad', - 'GravityLoad', - 'TributaryLoad', - 'HarmonicPointLoad', - 'HarmonicPressureLoad', - 'ThermalLoad', - - 'PrescribedTemperatureField', - - 'DeadLoad', - 'LiveLoad', - 'SuperImposedDeadLoad', - - '_Step', - '_GeneralStep', - '_Perturbation', - 'ModalAnalysis', - 'ComplexEigenValue', - 'StaticStep', - 'LinearStaticPerturbation', - 'BucklingAnalysis', - 'DynamicStep', - 'QuasiStaticStep', - 'DirectCyclicStep', - - 'FieldOutput', - 'HistoryOutput', + "Problem", + "GeneralDisplacement", + "Load", + "PrestressLoad", + "PointLoad", + "LineLoad", + "AreaLoad", + "GravityLoad", + "TributaryLoad", + "HarmonicPointLoad", + "HarmonicPressureLoad", + "ThermalLoad", + "Pattern", + "PrescribedField", + "PrescribedTemperatureField", + "DeadLoad", + "LiveLoad", + "SuperImposedDeadLoad", + "Step", + "GeneralStep", + "Perturbation", + "ModalAnalysis", + "ComplexEigenValue", + "StaticStep", + "LinearStaticPerturbation", + "BucklingAnalysis", + "DynamicStep", + "QuasiStaticStep", + "DirectCyclicStep", + "FieldOutput", + "HistoryOutput", ] diff --git a/src/compas_fea2/problem/displacements.py b/src/compas_fea2/problem/displacements.py index 836acf8ca..54be3db8d 100644 --- a/src/compas_fea2/problem/displacements.py +++ b/src/compas_fea2/problem/displacements.py @@ -8,10 +8,6 @@ class GeneralDisplacement(FEAData): """GeneralDisplacement object. - Note - ---- - Displacements are registered to a :class:`compas_fea2.problem.Step`. - Parameters ---------- name : str, optional @@ -51,9 +47,14 @@ class GeneralDisplacement(FEAData): zz component of moment, by default 0. axes : str, optional BC applied via 'local' or 'global' axes, by default 'global'. + + Notes + ----- + Displacements are registered to a :class:`compas_fea2.problem.Step`. + """ - def __init__(self, x=0, y=0, z=0, xx=0, yy=0, zz=0, axes='global', name=None, **kwargs): + def __init__(self, x=0, y=0, z=0, xx=0, yy=0, zz=0, axes="global", name=None, **kwargs): super(GeneralDisplacement, self).__init__(name=name, **kwargs) self.x = x self.y = y @@ -73,4 +74,4 @@ def axes(self, value): @property def components(self): - return {c: getattr(self, c) for c in ['x', 'y', 'z', 'xx', 'yy', 'zz']} + return {c: getattr(self, c) for c in ["x", "y", "z", "xx", "yy", "zz"]} diff --git a/src/compas_fea2/problem/fields.py b/src/compas_fea2/problem/fields.py index fafd5cc86..be8a9acab 100644 --- a/src/compas_fea2/problem/fields.py +++ b/src/compas_fea2/problem/fields.py @@ -5,21 +5,21 @@ from compas_fea2.base import FEAData -class _PrescribedField(FEAData): +class PrescribedField(FEAData): """Base class for all predefined initial conditions. - Note - ---- + Notes + ----- Fields are registered to a :class:`compas_fea2.problem.Step`. + """ def __init__(self, name=None, **kwargs): - super(_PrescribedField, self).__init__(name=name, **kwargs) + super(PrescribedField, self).__init__(name=name, **kwargs) -class PrescribedTemperatureField(_PrescribedField): - """Temperature field - """ +class PrescribedTemperatureField(PrescribedField): + """Temperature field""" def __init__(self, temperature, name=None, **kwargs): super(PrescribedTemperatureField, self).__init__(name, **kwargs) diff --git a/src/compas_fea2/problem/loads.py b/src/compas_fea2/problem/loads.py index 6cc3f280f..4c5cc760d 100644 --- a/src/compas_fea2/problem/loads.py +++ b/src/compas_fea2/problem/loads.py @@ -7,13 +7,9 @@ # TODO: make units independent using the utilities function -class _Load(FEAData): +class Load(FEAData): """Initialises base Load object. - Note - ---- - Loads are registered to a :class:`compas_fea2.problem.Pattern`. - Parameters ---------- name : str @@ -33,10 +29,15 @@ class _Load(FEAData): Load components. These differ according to each Load type axes : str, optional Load applied via 'local' or 'global' axes, by default 'global'. + + Notes + ----- + Loads are registered to a :class:`compas_fea2.problem.Pattern`. + """ - def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): - super(_Load, self).__init__(name=name, **kwargs) + def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes="global", name=None, **kwargs): + super(Load, self).__init__(name=name, **kwargs) self._axes = axes self.x = x self.y = y @@ -47,11 +48,11 @@ def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='glob def __rmul__(self, other): if isinstance(other, (float, int)): - components = ['x', 'y', 'z', 'xx', 'yy', 'zz'] + components = ["x", "y", "z", "xx", "yy", "zz"] for component in components: value = getattr(self, component) if value: - setattr(self, component, other*value) + setattr(self, component, other * value) return self @property @@ -64,7 +65,7 @@ def axes(self, value): @property def components(self): - keys = ['x', 'y', 'z', 'xx', 'yy', 'zz'] + keys = ["x", "y", "z", "xx", "yy", "zz"] return {key: getattr(self, key) for key in keys} @components.setter @@ -88,7 +89,8 @@ def problem(self): def model(self): return self.problem._registration -class PointLoad(_Load): + +class PointLoad(Load): """Concentrated forces and moments [units:N, Nm] applied to node(s). Parameters @@ -129,11 +131,11 @@ class PointLoad(_Load): Load applied via 'local' or 'global' axes. """ - def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): + def __init__(self, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes="global", name=None, **kwargs): super(PointLoad, self).__init__(x=x, y=y, z=z, xx=xx, yy=yy, zz=zz, axes=axes, name=name, **kwargs) -class LineLoad(_Load): +class LineLoad(Load): """Distributed line forces and moments [units:N/m or Nm/m] applied to element(s). Parameters @@ -177,12 +179,13 @@ class LineLoad(_Load): Load applied via 'local' or 'global' axes, by default 'global'. """ - def __init__(self, x=0, y=0, z=0, xx=0, yy=0, zz=0, axes='global', name=None, **kwargs): - super(LineLoad, self).__init__(components={ - 'x': x, 'y': y, 'z': z, 'xx': xx, 'yy': yy, 'zz': zz}, axes=axes, name=name, **kwargs) + def __init__(self, x=0, y=0, z=0, xx=0, yy=0, zz=0, axes="global", name=None, **kwargs): + super(LineLoad, self).__init__( + components={"x": x, "y": y, "z": z, "xx": xx, "yy": yy, "zz": zz}, axes=axes, name=name, **kwargs + ) -class AreaLoad(_Load): +class AreaLoad(Load): """Distributed area force [e.g. units:N/m2] applied to element(s). Parameters @@ -211,17 +214,13 @@ class AreaLoad(_Load): z component of area load. """ - def __init__(self, x=0, y=0, z=0, axes='local', name=None, **kwargs): - super(AreaLoad, self).__init__(components={'x': x, 'y': y, 'z': z}, axes=axes, name=name, **kwargs) + def __init__(self, x=0, y=0, z=0, axes="local", name=None, **kwargs): + super(AreaLoad, self).__init__(components={"x": x, "y": y, "z": z}, axes=axes, name=name, **kwargs) -class GravityLoad(_Load): +class GravityLoad(Load): """Gravity load [units:N/m3] applied to element(s). - Note - ---- - By default gravity is supposed to act along the negative `z` axis. - Parameters ---------- elements : str, list @@ -250,10 +249,15 @@ class GravityLoad(_Load): Factor to apply to y direction. z : float Factor to apply to z direction. + + Notes + ----- + By default gravity is supposed to act along the negative `z` axis. + """ def __init__(self, g, x=0, y=0, z=-1, name=None, **kwargs): - super(GravityLoad, self).__init__(x=x, y=y, z=z, axes='global', name=name, **kwargs) + super(GravityLoad, self).__init__(x=x, y=y, z=z, axes="global", name=name, **kwargs) self._g = g @property @@ -261,40 +265,40 @@ def g(self): return self._g -class PrestressLoad(_Load): +class PrestressLoad(Load): """Prestress load""" - def __init__(self, components, axes='global', name=None, **kwargs): + def __init__(self, components, axes="global", name=None, **kwargs): super(TributaryLoad, self).__init__(components, axes, name, **kwargs) raise NotImplementedError -class ThermalLoad(_Load): +class ThermalLoad(Load): """Thermal load""" - def __init__(self, components, axes='global', name=None, **kwargs): + def __init__(self, components, axes="global", name=None, **kwargs): super(ThermalLoad, self).__init__(components, axes, name, **kwargs) -class TributaryLoad(_Load): +class TributaryLoad(Load): """Tributary load""" - def __init__(self, components, axes='global', name=None, **kwargs): + def __init__(self, components, axes="global", name=None, **kwargs): super(TributaryLoad, self).__init__(components, axes, name, **kwargs) raise NotImplementedError -class HarmonicPointLoad(_Load): +class HarmonicPointLoad(Load): """""" - def __init__(self, components, axes='global', name=None, **kwargs): + def __init__(self, components, axes="global", name=None, **kwargs): super(HarmonicPointLoad, self).__init__(components, axes, name, **kwargs) raise NotImplementedError -class HarmonicPressureLoad(_Load): +class HarmonicPressureLoad(Load): """""" - def __init__(self, components, axes='global', name=None, **kwargs): + def __init__(self, components, axes="global", name=None, **kwargs): super(HarmonicPressureLoad, self).__init__(components, axes, name, **kwargs) raise NotImplementedError diff --git a/src/compas_fea2/problem/outputs.py b/src/compas_fea2/problem/outputs.py index 57f5f5884..3122c72af 100644 --- a/src/compas_fea2/problem/outputs.py +++ b/src/compas_fea2/problem/outputs.py @@ -6,21 +6,22 @@ from itertools import chain -class _Output(FEAData): +class Output(FEAData): """Base class for output requests. - Note - ---- - Outputs are registered to a :class:`compas_fea2.problem._Step`. - Parameters ---------- FEAData : _type_ _description_ + + Notes + ----- + Outputs are registered to a :class:`compas_fea2.problem.Step`. + """ def __init__(self, name=None, **kwargs): - super(_Output, self).__init__(name=name, **kwargs) + super(Output, self).__init__(name=name, **kwargs) @property def step(self): @@ -34,7 +35,8 @@ def problem(self): def model(self): return self.problem._registration -class FieldOutput(_Output): + +class FieldOutput(Output): """FieldOutput object for specification of the fields (stresses, displacements, etc..) to output from the analysis. @@ -54,6 +56,7 @@ class FieldOutput(_Output): list of node fields to output elements_outputs : list list of elements fields to output + """ def __init__(self, node_outputs=None, element_outputs=None, contact_outputs=None, name=None, **kwargs): @@ -78,7 +81,8 @@ def contact_outputs(self): def outputs(self): return chain(self.node_outputs, self.element_outputs, self.contact_outputs) -class HistoryOutput(_Output): + +class HistoryOutput(Output): """HistoryOutput object for recording the fields (stresses, displacements, etc..) from the analysis. @@ -93,7 +97,8 @@ class HistoryOutput(_Output): name : str Uniqe identifier. If not provided it is automatically generated. Set a name if you want a more human-readable input file. + """ - def __init__(self, name=None, **kwargs): + def __init__(self, name=None, **kwargs): super(HistoryOutput, self).__init__(name=name, **kwargs) diff --git a/src/compas_fea2/problem/patterns.py b/src/compas_fea2/problem/patterns.py index 002c3415a..3cccbdb96 100644 --- a/src/compas_fea2/problem/patterns.py +++ b/src/compas_fea2/problem/patterns.py @@ -8,32 +8,33 @@ class Pattern(FEAData): + """A pattern is the spatial distribution of a specific set of forces, + displacements, temperatures, and other effects which act on a structure. + Any combination of nodes and elements may be subjected to loading and + kinematic conditions. + + Parameters + ---------- + value : :class:`compas_fea2.problem.Load` | :class:`compas_fea2.problem.GeneralDisplacement` + The load/displacement of the pattern + distribution : list + list of :class:`compas_fea2.model.Node` or :class:`compas_fea2.model.Element` + name : str + Uniqe identifier. If not provided it is automatically generated. Set a + name if you want a more human-readable input file. + + Attributes + ---------- + value : :class:`compas_fea2.problem.Load` + The load of the pattern + distribution : list + list of :class:`compas_fea2.model.Node` or :class:`compas_fea2.model.Element` + name : str + Uniqe identifier. + + """ def __init__(self, value, distribution, name=None, **kwargs): - """A pattern is the spatial distribution of a specific set of forces, - displacements, temperatures, and other effects which act on a structure. - Any combination of nodes and elements may be subjected to loading and - kinematic conditions. - - Parameters - ---------- - value : :class:`compas_fea2.problem._Load` | :class:`compas_fea2.problem.GeneralDisplacement` - The load/displacement of the pattern - distribution : list - list of :class:`compas_fea2.model.Node` or :class:`compas_fea2.model._Element` - name : str - Uniqe identifier. If not provided it is automatically generated. Set a - name if you want a more human-readable input file. - - Attributes - ---------- - value : :class:`compas_fea2.problem._Load` - The load of the pattern - distribution : list - list of :class:`compas_fea2.model.Node` or :class:`compas_fea2.model._Element` - name : str - Uniqe identifier. - """ super(Pattern, self).__init__(name, **kwargs) self._load = value value._registration = self diff --git a/src/compas_fea2/problem/problem.py b/src/compas_fea2/problem/problem.py index c1207e41f..eabfd9d9b 100644 --- a/src/compas_fea2/problem/problem.py +++ b/src/compas_fea2/problem/problem.py @@ -2,52 +2,25 @@ from __future__ import division from __future__ import print_function -import pickle +import os import compas_fea2 from pathlib import Path -import os from typing import Iterable -from unittest import result + +from compas.geometry import Vector +from compas.geometry import sum_vectors from compas_fea2.base import FEAData -from compas_fea2.problem.steps.step import _Step +from compas_fea2.problem.steps.step import Step from compas_fea2.job.input_file import InputFile - from compas_fea2.utilities._utils import timer -from compas_fea2.utilities._utils import step_method - from compas_fea2.results import NodeFieldResults -from compas.geometry import Point, Plane -from compas.geometry import Vector -from compas.geometry import sum_vectors - - class Problem(FEAData): - """A Problem is a collection of analysis steps (:class:`compas_fea2.problem._Step) + """A Problem is a collection of analysis steps (:class:`compas_fea2.problem.Step) applied in a specific sequence. - Note - ---- - Problems are registered to a :class:`compas_fea2.model.Model`. - - Problems can also be used as canonical `load combinations`, where each `load` - is actually a `factored step`. For example, a typical load combination such - as 1.35*DL+1.50LL can be applied to the model by creating the Steps DL and LL, - factoring them (see :class:`compas_fea2.problem._Step documentation) and adding - them to Problme - - Note - ---- - While for linear models the sequence of the steps is irrelevant, it is not the - case for non-linear models. - - Warning - ------- - Factore Steps are new objects! check the :class:`compas_fea2.problem._Step - documentation. - Parameters ---------- name : str, optional @@ -67,13 +40,31 @@ class Problem(FEAData): describption : str Brief description of the Problem. This will be added to the input file and can be useful for future reference. - steps : list of :class:`compas_fea2.problem._Step` + steps : list of :class:`compas_fea2.problem.Step` list of analysis steps in the order they are applied. path : str, :class:`pathlib.Path` Path to the analysis folder where all the files will be saved. results : :class:`compas_fea2.results.Results` Results object with the analyisis results. + Notes + ----- + Problems are registered to a :class:`compas_fea2.model.Model`. + + Problems can also be used as canonical `load combinations`, where each `load` + is actually a `factored step`. For example, a typical load combination such + as 1.35*DL+1.50LL can be applied to the model by creating the Steps DL and LL, + factoring them (see :class:`compas_fea2.problem.Step documentation) and adding + them to Problme + + While for linear models the sequence of the steps is irrelevant, it is not the + case for non-linear models. + + Warnings + -------- + Factore Steps are new objects! check the :class:`compas_fea2.problem.Step + documentation. + """ def __init__(self, name=None, description=None, **kwargs): @@ -96,10 +87,11 @@ def steps(self): @property def path(self): return self._path + @path.setter def path(self, value): self._path = value if isinstance(value, Path) else Path(value) - self._path_db = os.path.join(self._path, '{}-results.db'.format(self.name)) + self._path_db = os.path.join(self._path, "{}-results.db".format(self.name)) @property def db_connection(self): @@ -112,19 +104,20 @@ def path_db(self): @property def steps_order(self): return self._steps_order + @steps_order.setter def steps_order(self, value): for step in value: if not self.is_step_in_problem(step, add=False): - raise ValueError('{!r} must be previously added to {!r}'.format(step, self)) + raise ValueError("{!r} must be previously added to {!r}".format(step, self)) self._steps_order = value - # ========================================================================= # Step methods # ========================================================================= + def find_step_by_name(self, name): - # type: (str) -> _Step + # type: (str) -> Step """Find if there is a step with the given name in the problem. Parameters @@ -133,7 +126,7 @@ def find_step_by_name(self, name): Returns ------- - :class:`compas_fea2.problem._Step` + :class:`compas_fea2.problem.Step` """ for step in self.steps: @@ -141,16 +134,16 @@ def find_step_by_name(self, name): return step def is_step_in_problem(self, step, add=True): - """Check if a :class:`compas_fea2.problem._Step` is defined in the Problem. + """Check if a :class:`compas_fea2.problem.Step` is defined in the Problem. Parameters ---------- - step : :class:`compas_fea2.problem._Step` + step : :class:`compas_fea2.problem.Step` The Step object to find. Returns ------- - :class:`compas_fea2.problem._Step` + :class:`compas_fea2.problem.Step` Raises ------ @@ -161,36 +154,36 @@ def is_step_in_problem(self, step, add=True): name of a Step already defined in the Problem. """ - if not isinstance(step, _Step): - raise TypeError('{!r} is not a Step'.format(step)) + if not isinstance(step, Step): + raise TypeError("{!r} is not a Step".format(step)) if step not in self.steps: - print('{!r} not found'.format(step)) + print("{!r} not found".format(step)) if add: step = self.add_step(step) - print('{!r} added to the Problem'.format(step)) + print("{!r} added to the Problem".format(step)) return step return False return True def add_step(self, step): # # type: (Step) -> Step - """Adds a :class:`compas_fea2.problem._Step` to the problem. The name of + """Adds a :class:`compas_fea2.problem.Step` to the problem. The name of the Step must be unique Parameters ---------- - Step : :class:`compas_fea2.problem._Step` + Step : :class:`compas_fea2.problem.Step` The analysis step to add to the problem. Returns ------- - :class:`compas_fea2.problem._Step` + :class:`compas_fea2.problem.Step` """ - if not isinstance(step, _Step): - raise TypeError('You must provide a valid compas_fea2 Step object') + if not isinstance(step, Step): + raise TypeError("You must provide a valid compas_fea2 Step object") if self.find_step_by_name(step): - raise ValueError('There is already a step with the same name in the model.') + raise ValueError("There is already a step with the same name in the model.") step._key = len(self._steps) self._steps.add(step) @@ -199,16 +192,16 @@ def add_step(self, step): return step def add_steps(self, steps): - """Adds multiple :class:`compas_fea2.problem._Step` objects to the problem. + """Adds multiple :class:`compas_fea2.problem.Step` objects to the problem. Parameters ---------- - steps : list[:class:`compas_fea2.problem._Step`] + steps : list[:class:`compas_fea2.problem.Step`] List of steps objects in the order they will be applied. Returns ------- - list[:class:`compas_fea2.problem._Step`] + list[:class:`compas_fea2.problem.Step`] """ return [self.add_step(step) for step in steps] @@ -230,18 +223,13 @@ def add_steps(self, steps): # Not implemented yet! # """ # for step in order: - # if not isinstance(step, _Step): + # if not isinstance(step, Step): # raise TypeError('{} is not a step'.format(step)) # self._steps_order = order def add_linear_perturbation_step(self, lp_step, base_step): """Add a linear perturbation step to a previously defined step. - Note - ---- - Linear perturbartion steps do not change the history of the problem (hence - following steps will not consider their effects). - Parameters ---------- lp_step : obj @@ -249,6 +237,12 @@ def add_linear_perturbation_step(self, lp_step, base_step): base_step : str name of a previously defined step which will be used as starting conditions for the application of the linear perturbation step. + + Notes + ----- + Linear perturbartion steps do not change the history of the problem (hence + following steps will not consider their effects). + """ raise NotImplementedError @@ -269,7 +263,7 @@ def summary(self): str Problem summary """ - steps_data = '\n'.join([f'{step.name}' for step in self.steps]) + steps_data = "\n".join([f"{step.name}" for step in self.steps]) summary = """ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -284,17 +278,17 @@ def summary(self): Analysis folder path : {} -""".format(self._name, - self.description or 'N/A', - steps_data, - self.path or 'N/A') +""".format( + self._name, self.description or "N/A", steps_data, self.path or "N/A" + ) print(summary) return summary # ========================================================================= # Analysis methods # ========================================================================= - @timer(message='Finished writing input file in') + + @timer(message="Finished writing input file in") def write_input_file(self, path=None): # type: (Path |str) -> None """Writes the input file. @@ -325,15 +319,17 @@ def _check_analysis_path(self, path): path : :class:`pathlib.Path` Path where the input file will be saved. - Return + Returns + ------- :class:`pathlib.Path` Path where the input file will be saved. + """ if path: self.model.path = path self.path = self.model.path.joinpath(self.name) if not self.path and not self.model.path: - raise AttributeError('You must provide a path for storing the model and the analysis results.') + raise AttributeError("You must provide a path for storing the model and the analysis results.") return self.path def analyse(self, path=None, *args, **kwargs): @@ -343,12 +339,12 @@ def analyse(self, path=None, *args, **kwargs): ------ NotImplementedError This method is implemented only at the backend level. + """ raise NotImplementedError("this function is not available for the selected backend") def analyze(self, *args, **kwargs): - """American spelling of the analyse method \n""" - __doc__ += self.analyse.__doc__ + """American spelling of the analyse method""" self.analyse(*args, **kwargs) def analyse_and_extract(self, path=None, *args, **kwargs): @@ -362,16 +358,11 @@ def analyse_and_extract(self, path=None, *args, **kwargs): """ raise NotImplementedError("this function is not available for the selected backend") - #FIXME check the funciton and 'memory only parameter + # FIXME check the funciton and 'memory only parameter def analyse_and_store(self, memory_only=False, *args, **kwargs): """Analyse the problem in the selected backend and stores the results in the model. - Note - ---- - The extraction of the results to SQLite ca be done `in memory` to speed up - the process but no database file is generated. - Parameters ---------- problems : [:class:`compas_fea2.problem.Problem`], optional @@ -380,6 +371,12 @@ def analyse_and_store(self, memory_only=False, *args, **kwargs): memory_only : bool, optional store the SQLITE database only in memory (no .db file will be saved), by default False + + Notes + ----- + The extraction of the results to SQLite ca be done `in memory` to speed up + the process but no database file is generated. + """ self.analyse(*args, **kwargs) self.convert_results_to_sqlite(*args, **kwargs) @@ -389,11 +386,6 @@ def restart_analysis(self, *args, **kwargs): """Continue a previous analysis from a given increement with additional steps. - Note - ---- - For abaqus, you have to specify to save specific files during the original - analysis by passing the `restart=True` option. - Parameters ---------- problem : :class:`compas_fea2.problme.Problem` @@ -407,6 +399,12 @@ def restart_analysis(self, *args, **kwargs): ------ ValueError _description_ + + Notes + ----- + For abaqus, you have to specify to save specific files during the original + analysis by passing the `restart=True` option. + """ raise NotImplementedError("this function is not available for the selected backend") @@ -414,8 +412,7 @@ def restart_analysis(self, *args, **kwargs): # Results methods - general # ========================================================================= - - @timer(message='Problem results copied in the model in ') + @timer(message="Problem results copied in the model in ") def store_results_in_model(self, database_path=None, database_name=None, steps=None, fields=None, *args, **kwargs): """Copy the results form the sqlite database back into the model at the nodal and element level. @@ -428,7 +425,7 @@ def store_results_in_model(self, database_path=None, database_name=None, steps=N name of the database file_format : str, optional serialization type ('pkl' or 'json'), by default 'pkl' - steps : :class:`compas_fea2.problem._Step`, optional + steps : :class:`compas_fea2.problem.Step`, optional The steps fro which copy the results, by default `None` (all the steps are saved) fields : _type_, optional Fields results to save, by default `None` (all available fields are saved) @@ -438,7 +435,9 @@ def store_results_in_model(self, database_path=None, database_name=None, steps=N None """ - databse_full_path = os.path.join(database_path, database_name) if database_path and database_name else self.path_results + databse_full_path = ( + os.path.join(database_path, database_name) if database_path and database_name else self.path_results + ) if not os.path.exists(databse_full_path): self.convert_results_to_sqlite(*args, **kwargs) for step in steps or self.steps: @@ -463,7 +462,7 @@ def get_reaction_forces_sql(self, step=None): """ if not step: step = self._steps_order[-1] - _, col_val = self._get_field_results('RF', step) + _, col_val = self._get_field_results("RF", step) return self._get_vector_results(col_val) def get_reaction_moments_sql(self, step=None): @@ -481,14 +480,14 @@ def get_reaction_moments_sql(self, step=None): """ if not step: step = self._steps_order[-1] - _, col_val = self._get_field_results('RM', step) + _, col_val = self._get_field_results("RM", step) return self._get_vector_results(col_val) # ========================================================================= # Results methods - displacements # ========================================================================= - # TODO add moments + # TODO add moments def get_total_reaction(self): reactions_forces = [] for part in self.step.problem.model.parts: @@ -507,8 +506,9 @@ def get_total_moment(self): def get_deformed_model(self, step=None, **kwargs): from copy import deepcopy + if not step: - step=self.steps_order[-1] + step = self.steps_order[-1] deformed_model = deepcopy(self.model) # # # TODO create a copy of the model first @@ -520,10 +520,10 @@ def get_deformed_model(self, step=None, **kwargs): raise NotImplementedError() return deformed_model - # ========================================================================= # Viewer methods # ========================================================================= + # def show(self, scale_factor=1., step=None, width=1600, height=900, parts=None, # solid=True, draw_nodes=False, node_labels=False, # draw_bcs=1., draw_constraints=True, draw_loads=True, **kwargs): @@ -562,18 +562,20 @@ def get_deformed_model(self, step=None, **kwargs): # v.draw_loads(step, scale_factor=kwargs['draw_loads']) # v.show() - def show_nodes_field_vector(self, field_name, vector_sf=1., model_sf=1., step=None, width=1600, height=900, **kwargs): + def show_nodes_field_vector( + self, field_name, vector_sf=1.0, model_sf=1.0, step=None, width=1600, height=900, **kwargs + ): from compas_fea2.UI.viewer import FEA2Viewer - from compas.colors import ColorMap, Color - cmap = kwargs.get('cmap', ColorMap.from_palette('hawaii')) - #ColorMap.from_color(Color.red(), rangetype='light') #ColorMap.from_mpl('viridis') + from compas.colors import ColorMap + + cmap = kwargs.get("cmap", ColorMap.from_palette("hawaii")) # Get values if not step: step = self._steps_order[-1] field = NodeFieldResults(field_name, step) - min_value = field._min_invariants['magnitude'].invariants["MIN(magnitude)"] - max_value = field._max_invariants['magnitude'].invariants["MAX(magnitude)"] + min_value = field._min_invariants["magnitude"].invariants["MIN(magnitude)"] + max_value = field._max_invariants["magnitude"].invariants["MAX(magnitude)"] # Color the mesh pts, vectors, colors = [], [], [] @@ -582,19 +584,19 @@ def show_nodes_field_vector(self, field_name, vector_sf=1., model_sf=1., step=No continue vectors.append(r.vector.scaled(vector_sf)) pts.append(r.location.xyz) - colors.append(cmap(r.invariants['magnitude'], minval=min_value, maxval=max_value)) + colors.append(cmap(r.invariants["magnitude"], minval=min_value, maxval=max_value)) # Display results v = FEA2Viewer(width, height, scale_factor=model_sf) v.draw_nodes_vector(pts=pts, vectors=vectors, colors=colors) v.draw_parts(self.model.parts) - if kwargs.get('draw_bcs', None): - v.draw_bcs(self.model, scale_factor=kwargs['draw_bcs']) - if kwargs.get('draw_loads', None): - v.draw_loads(step, scale_factor=kwargs['draw_loads']) + if kwargs.get("draw_bcs", None): + v.draw_bcs(self.model, scale_factor=kwargs["draw_bcs"]) + if kwargs.get("draw_loads", None): + v.draw_loads(step, scale_factor=kwargs["draw_loads"]) v.show() - def show_nodes_field(self, field_name, component, step=None, width=1600, height=900, model_sf=1., **kwargs): + def show_nodes_field(self, field_name, component, step=None, width=1600, height=900, model_sf=1.0, **kwargs): """Display a contour plot of a given field and component. The field must de defined at the nodes of the model (e.g displacement field). @@ -632,65 +634,69 @@ def show_nodes_field(self, field_name, component, step=None, width=1600, height= ------ ValueError _description_ + """ from compas_fea2.UI.viewer import FEA2Viewer from compas.colors import ColorMap, Color - cmap = kwargs.get('cmap', ColorMap.from_palette('hawaii')) - #ColorMap.from_color(Color.red(), rangetype='light') #ColorMap.from_mpl('viridis') + + cmap = kwargs.get("cmap", ColorMap.from_palette("hawaii")) + # ColorMap.from_color(Color.red(), rangetype='light') #ColorMap.from_mpl('viridis') # Get mesh - parts_gkey_vertex={} - parts_mesh={} + parts_gkey_vertex = {} + parts_mesh = {} for part in self.model.parts: - if (mesh:= part.discretized_boundary_mesh): + if mesh := part.discretized_boundary_mesh: colored_mesh = mesh.copy() parts_gkey_vertex[part.name] = colored_mesh.gkey_key(compas_fea2.PRECISION) parts_mesh[part.name] = colored_mesh else: - raise AttributeError('Discretized boundary mesh not found') + raise AttributeError("Discretized boundary mesh not found") # Set the bounding limits - if kwargs.get('bound', None): - if not isinstance(kwargs['bound'], Iterable) or len(kwargs['bound'])!=2: - raise ValueError('You need to provide an upper and lower bound -> (lb, up)') - if kwargs['bound'][0]>kwargs['bound'][1]: - kwargs['bound'][0], kwargs['bound'][1] = kwargs['bound'][1], kwargs['bound'][0] + if kwargs.get("bound", None): + if not isinstance(kwargs["bound"], Iterable) or len(kwargs["bound"]) != 2: + raise ValueError("You need to provide an upper and lower bound -> (lb, up)") + if kwargs["bound"][0] > kwargs["bound"][1]: + kwargs["bound"][0], kwargs["bound"][1] = kwargs["bound"][1], kwargs["bound"][0] # Get values if not step: step = self._steps_order[-1] field = NodeFieldResults(field_name, step) - min_value = field._min_components[component].components[f'MIN({component})'] - max_value = field._max_components[component].components[f'MAX({component})'] + min_value = field._min_components[component].components[f"MIN({component})"] + max_value = field._max_components[component].components[f"MAX({component})"] # Color the mesh for r in field.results: - if min_value - max_value == 0.: + if min_value - max_value == 0.0: color = Color.red() - elif kwargs.get('bound', None): - if r.components[component]>=kwargs['bound'] or r.components[component]<=kwargs['bound']: + elif kwargs.get("bound", None): + if r.components[component] >= kwargs["bound"] or r.components[component] <= kwargs["bound"]: color = Color.red() else: color = cmap(r.components[component], minval=min_value, maxval=max_value) else: color = cmap(r.components[component], minval=min_value, maxval=max_value) if r.location.gkey in parts_gkey_vertex[part.name]: - parts_mesh[part.name].vertex_attribute(parts_gkey_vertex[part.name][r.location.gkey], 'color', color) + parts_mesh[part.name].vertex_attribute(parts_gkey_vertex[part.name][r.location.gkey], "color", color) # Display results v = FEA2Viewer(width, height, scale_factor=model_sf) for part in self.model.parts: v.draw_mesh(parts_mesh[part.name]) - if kwargs.get('draw_bcs', None): - v.draw_bcs(self.model, scale_factor=kwargs['draw_bcs']) + if kwargs.get("draw_bcs", None): + v.draw_bcs(self.model, scale_factor=kwargs["draw_bcs"]) - if kwargs.get('draw_loads', None): - v.draw_loads(step, scale_factor=kwargs['draw_loads']) + if kwargs.get("draw_loads", None): + v.draw_loads(step, scale_factor=kwargs["draw_loads"]) v.show() - def show_displacements(self, component=3, step=None, style='contour', deformed=False, width=1600, height=900, model_sf=1., **kwargs): + def show_displacements( + self, component=3, step=None, style="contour", deformed=False, width=1600, height=900, model_sf=1.0, **kwargs + ): """Display the displacement of the nodes. Parameters @@ -723,21 +729,30 @@ def show_displacements(self, component=3, step=None, style='contour', deformed=F Raises ------ ValueError - "The style can be either 'vector' or 'contour'" + The style can be either 'vector' or 'contour'. + """ - if style == 'contour': - self.show_nodes_field(field_name='U', component='U'+str(component), step=step, width=width, height=height, model_sf=model_sf, **kwargs) - elif style == 'vector': - raise NotImplementedError('WIP') + if style == "contour": + self.show_nodes_field( + field_name="U", + component="U" + str(component), + step=step, + width=width, + height=height, + model_sf=model_sf, + **kwargs, + ) + elif style == "vector": + raise NotImplementedError("WIP") else: raise ValueError("The style can be either 'vector' or 'contour'") - def show_deformed(self, step=None, width=1600, height=900, scale_factor=1., **kwargs): + def show_deformed(self, step=None, width=1600, height=900, scale_factor=1.0, **kwargs): """Display the structure in its deformed configuration. Parameters ---------- - step : :class:`compas_fea2.problem._Step`, optional + step : :class:`compas_fea2.problem.Step`, optional The Step of the analysis, by default None. If not provided, the last step is used. width : int, optional @@ -745,27 +760,27 @@ def show_deformed(self, step=None, width=1600, height=900, scale_factor=1., **kw height : int, optional Height of the viewer window, by default 900 - Return - ------ + Returns + ------- None + """ from compas_fea2.UI.viewer import FEA2Viewer - from compas.geometry import Point, Vector + from compas.geometry import Vector - from compas.colors import ColorMap, Color v = FEA2Viewer(width, height) if not step: - step=self.steps_order[-1] + step = self.steps_order[-1] # TODO create a copy of the model first - displacements = NodeFieldResults('U', step) + displacements = NodeFieldResults("U", step) for displacement in displacements.results: vector = displacement.vector.scaled(scale_factor) displacement.location.xyz = sum_vectors([Vector(*displacement.location.xyz), vector]) v.draw_parts(self.model.parts, solid=True) - if kwargs.get('draw_bcs', None): - v.draw_bcs(self.model, scale_factor=kwargs['draw_bcs']) + if kwargs.get("draw_bcs", None): + v.draw_bcs(self.model, scale_factor=kwargs["draw_bcs"]) - if kwargs.get('draw_loads', None): - v.draw_loads(step, scale_factor=kwargs['draw_loads']) + if kwargs.get("draw_loads", None): + v.draw_loads(step, scale_factor=kwargs["draw_loads"]) v.show() diff --git a/src/compas_fea2/problem/steps/__init__.py b/src/compas_fea2/problem/steps/__init__.py index 1e6ba537b..e69de29bb 100644 --- a/src/compas_fea2/problem/steps/__init__.py +++ b/src/compas_fea2/problem/steps/__init__.py @@ -1,32 +0,0 @@ -from __future__ import absolute_import -from __future__ import division -from __future__ import print_function - -from .step import ( - _Step, - _GeneralStep, -) - -from .static import ( - StaticStep, - StaticRiksStep, -) - -from .dynamic import ( - DynamicStep, -) - -from .quasistatic import ( - QuasiStaticStep, - DirectCyclicStep, -) - -from .perturbations import ( - _Perturbation, - ModalAnalysis, - ComplexEigenValue, - BucklingAnalysis, - LinearStaticPerturbation, - StedyStateDynamic, - SubstructureGeneration, -) diff --git a/src/compas_fea2/problem/steps/dynamic.py b/src/compas_fea2/problem/steps/dynamic.py index ff8331d8d..b9195e076 100644 --- a/src/compas_fea2/problem/steps/dynamic.py +++ b/src/compas_fea2/problem/steps/dynamic.py @@ -2,22 +2,11 @@ from __future__ import division from __future__ import print_function -from compas_fea2.base import FEAData -from .step import _Step +from .step import Step -class DynamicStep(_Step): - """Step for dynamic analysis. - - Parameters - ---------- - None - - Attributes - ---------- - None - - """ +class DynamicStep(Step): + """Step for dynamic analysis.""" def __init__(self, name=None, **kwargs): super(DynamicStep, self).__init__(name=name, **kwargs) diff --git a/src/compas_fea2/problem/steps/perturbations.py b/src/compas_fea2/problem/steps/perturbations.py index 0f7ab8131..80ec97db3 100644 --- a/src/compas_fea2/problem/steps/perturbations.py +++ b/src/compas_fea2/problem/steps/perturbations.py @@ -2,26 +2,25 @@ from __future__ import division from __future__ import print_function -from compas_fea2.base import FEAData -from .step import _Step +from .step import Step -class _Perturbation(_Step): +class Perturbation(Step): """A perturbation is a change of the state of the structure after an analysis step. Differently from Steps, perturbations' changes are not carried over to the next step. Parameters ---------- - _Step : _type_ + Step : _type_ _description_ """ def __init__(self, name=None, **kwargs): - super(_Perturbation, self).__init__(name=name, **kwargs) + super(Perturbation, self).__init__(name=name, **kwargs) -class ModalAnalysis(_Perturbation): +class ModalAnalysis(Perturbation): """Perform a modal analysis of the Model from the resulting state after an analysis Step. @@ -31,6 +30,7 @@ class ModalAnalysis(_Perturbation): Name of the ModalStep. modes : int Number of modes to analyse. + """ def __init__(self, modes=1, name=None, **kwargs): @@ -38,7 +38,7 @@ def __init__(self, modes=1, name=None, **kwargs): self.modes = modes -class ComplexEigenValue(_Perturbation): +class ComplexEigenValue(Perturbation): """""" def __init__(self, name=None, **kwargs): @@ -46,7 +46,7 @@ def __init__(self, name=None, **kwargs): raise NotImplementedError -class BucklingAnalysis(_Perturbation): +class BucklingAnalysis(Perturbation): """""" def __init__(self, modes, vectors=None, iterations=30, algorithm=None, name=None, **kwargs): @@ -57,20 +57,20 @@ def __init__(self, modes, vectors=None, iterations=30, algorithm=None, name=None self._algorithm = algorithm def _compute_vectors(self, modes): - self._vectors = modes*2 + self._vectors = modes * 2 if modes > 9: self._vectors += modes @staticmethod def Lanczos(modes, name=None): - return BucklingAnalysis(modes=modes, vectors=None, algorithhm='Lanczos', name=name) + return BucklingAnalysis(modes=modes, vectors=None, algorithhm="Lanczos", name=name) @staticmethod def Subspace(modes, iterations, vectors=None, name=None): - return BucklingAnalysis(modes=modes, vectors=vectors, iterations=iterations, algorithhm='Subspace', name=name) + return BucklingAnalysis(modes=modes, vectors=vectors, iterations=iterations, algorithhm="Subspace", name=name) -class LinearStaticPerturbation(_Perturbation): +class LinearStaticPerturbation(Perturbation): """""" def __init__(self, name=None, **kwargs): @@ -78,7 +78,7 @@ def __init__(self, name=None, **kwargs): raise NotImplementedError -class StedyStateDynamic(_Perturbation): +class StedyStateDynamic(Perturbation): """""" def __init__(self, name=None, **kwargs): @@ -86,7 +86,7 @@ def __init__(self, name=None, **kwargs): raise NotImplementedError -class SubstructureGeneration(_Perturbation): +class SubstructureGeneration(Perturbation): """""" def __init__(self, name=None, **kwargs): diff --git a/src/compas_fea2/problem/steps/quasistatic.py b/src/compas_fea2/problem/steps/quasistatic.py index 7855b9b5f..19ccf1158 100644 --- a/src/compas_fea2/problem/steps/quasistatic.py +++ b/src/compas_fea2/problem/steps/quasistatic.py @@ -2,40 +2,19 @@ from __future__ import division from __future__ import print_function -from compas_fea2.base import FEAData -from .step import _Step +from .step import Step -class QuasiStaticStep(_Step): - """Step for quasi-static analysis. - - Parameters - ---------- - None - - Attributes - ---------- - None - - """ +class QuasiStaticStep(Step): + """Step for quasi-static analysis.""" def __init__(self, name=None, **kwargs): super(QuasiStaticStep, self).__init__(name=name, **kwargs) raise NotImplementedError -class DirectCyclicStep(_Step): - """Step for a direct cyclic analysis. - - Parameters - ---------- - None - - Attributes - ---------- - None - - """ +class DirectCyclicStep(Step): + """Step for a direct cyclic analysis.""" def __init__(self, name=None, **kwargs): super(DirectCyclicStep, self).__init__(name=name, **kwargs) diff --git a/src/compas_fea2/problem/steps/static.py b/src/compas_fea2/problem/steps/static.py index 4f7c72191..598524f83 100644 --- a/src/compas_fea2/problem/steps/static.py +++ b/src/compas_fea2/problem/steps/static.py @@ -2,24 +2,18 @@ from __future__ import division from __future__ import print_function from typing import Iterable -from xml.dom.minidom import Element from compas_fea2.model.nodes import Node -from compas_fea2.model.groups import ElementsGroup, PartsGroup - - -from compas_fea2.problem.loads import _Load from compas_fea2.problem.loads import GravityLoad from compas_fea2.problem.loads import PointLoad - from compas_fea2.problem.patterns import Pattern from compas_fea2.problem.displacements import GeneralDisplacement from compas_fea2.problem.fields import PrescribedTemperatureField -from .step import _GeneralStep +from .step import GeneralStep -class StaticStep(_GeneralStep): +class StaticStep(GeneralStep): """StaticStep for use in a static analysis. Parameters @@ -78,21 +72,47 @@ class StaticStep(_GeneralStep): Gravity load to assing to the whole model. displacements : dict Dictionary of the displacements assigned to each part in the model in the step. - """ - def __init__(self, max_increments=100, initial_inc_size=1, min_inc_size=0.00001, time=1, nlgeom=False, modify=True, name=None, **kwargs): - super(StaticStep, self).__init__(max_increments=max_increments, - initial_inc_size=initial_inc_size, min_inc_size=min_inc_size, - time=time, nlgeom=nlgeom, modify=modify, name=name, **kwargs) + """ - def add_node_load(self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): + def __init__( + self, + max_increments=100, + initial_inc_size=1, + min_inc_size=0.00001, + time=1, + nlgeom=False, + modify=True, + name=None, + **kwargs, + ): + super(StaticStep, self).__init__( + max_increments=max_increments, + initial_inc_size=initial_inc_size, + min_inc_size=min_inc_size, + time=time, + nlgeom=nlgeom, + modify=modify, + name=name, + **kwargs, + ) + + def add_node_load( + self, + nodes, + x=None, + y=None, + z=None, + xx=None, + yy=None, + zz=None, + axes="global", + name=None, + **kwargs, + ): """Add a :class:`compas_fea2.problem.PointLoad` subclass object to the ``Step`` at specific Nodes. - Warning - ------- - local axes are not supported yet - Parameters ---------- name : str @@ -112,22 +132,38 @@ def add_node_load(self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=None axes : str, optional 'local' or 'global' axes, by default 'global' - Return - ------ + Returns + ------- :class:`compas_fea2.problem.PointLoad` - """ - if axes != 'global': - raise NotImplementedError('local axes are not supported yet') - return self._add_pattern(Pattern(value=PointLoad(x, y, z, xx, yy, zz, axes, name, **kwargs), distribution=nodes)) - def add_point_load(self, points, tolerance=1000, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): + Warnings + -------- + local axes are not supported yet + + """ + if axes != "global": + raise NotImplementedError("local axes are not supported yet") + return self._add_pattern( + Pattern(value=PointLoad(x, y, z, xx, yy, zz, axes, name, **kwargs), distribution=nodes) + ) + + def add_point_load( + self, + points, + tolerance=1000, + x=None, + y=None, + z=None, + xx=None, + yy=None, + zz=None, + axes="global", + name=None, + **kwargs, + ): """Add a :class:`compas_fea2.problem.PointLoad` subclass object to the ``Step`` at specific points. - Warning - ------- - local axes are not supported yet - Parameters ---------- name : str @@ -147,33 +183,27 @@ def add_point_load(self, points, tolerance=1000, x=None, y=None, z=None, xx=None axes : str, optional 'local' or 'global' axes, by default 'global' - Return - ------ + Returns + ------- :class:`compas_fea2.problem.PointLoad` + + Warnings + -------- + local axes are not supported yet + """ - if axes != 'global': - raise NotImplementedError('local axes are not supported yet') + if axes != "global": + raise NotImplementedError("local axes are not supported yet") if not self.problem: - raise ValueError('No problem assigned to the step.') + raise ValueError("No problem assigned to the step.") if not self.model: - raise ValueError('No model assigned to the problem.') + raise ValueError("No model assigned to the problem.") nodes = [self.model.find_closest_nodes_to_point(point, distance=tolerance)[0][0] for point in points] self.add_node_load(nodes, x=x, y=y, z=z, xx=xx, yy=yy, zz=zz, axes=axes, name=name, **kwargs) - def add_gravity_load(self, g=9.81, x=0., y=0., z=-1.): + def add_gravity_load(self, g=9.81, x=0.0, y=0.0, z=-1.0): """Add a :class:`compas_fea2.problem.GravityLoad` load to the ``Step`` - Note - ---- - The gravity field is applied to the whole model. To remove parts of the - model from the calculation of the gravity force, you can assign to them - a 0 mass material. - - Warning - ------- - Be careful to assign a value of *g* consistent with the units in your - model! - Parameters ---------- g : float, optional @@ -186,6 +216,18 @@ def add_gravity_load(self, g=9.81, x=0., y=0., z=-1.): z component of the gravity direction vector (in global coordinates), by default -1. distribution : [:class:`compas_fea2.model.PartsGroup`] | [:class:`compas_fea2.model.ElementsGroup`] Group of parts or elements affected by gravity. + + Notes + ----- + The gravity field is applied to the whole model. To remove parts of the + model from the calculation of the gravity force, you can assign to them + a 0 mass material. + + Warnings + -------- + Be careful to assign a value of *g* consistent with the units in your + model! + """ # TODO implement distribution # if isinstance(distribution, PartsGroup): @@ -199,14 +241,24 @@ def add_gravity_load(self, g=9.81, x=0., y=0., z=-1.): def add_prestress_load(self): raise NotImplementedError - def add_line_load(self, polyline, discretization=10, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', distance=500, name=None, **kwargs): + def add_line_load( + self, + polyline, + discretization=10, + x=None, + y=None, + z=None, + xx=None, + yy=None, + zz=None, + axes="global", + distance=500, + name=None, + **kwargs, + ): """Add a :class:`compas_fea2.problem.PointLoad` subclass object to the ``Step`` along a prescribed path. - Warning - ------- - local axes are not supported yet - Parameters ---------- name : str @@ -231,44 +283,62 @@ def add_line_load(self, polyline, discretization=10, x=None, y=None, z=None, xx= axes : str, optional 'local' or 'global' axes, by default 'global' - Return - ------ + Returns + ------- :class:`compas_fea2.problem.PointLoad` + + Warnings + -------- + local axes are not supported yet + """ - if axes != 'global': - raise NotImplementedError('local axes are not supported yet') + if axes != "global": + raise NotImplementedError("local axes are not supported yet") if not self.problem: - raise ValueError('No problem assigned to the step.') + raise ValueError("No problem assigned to the step.") if not self.model: - raise ValueError('No model assigned to the problem.') - nodes = [self.model.find_closest_nodes_to_point(point, distance=distance)[0][0] for point in polyline.divide_polyline(discretization)] + raise ValueError("No model assigned to the problem.") + nodes = [ + self.model.find_closest_nodes_to_point(point, distance=distance)[0][0] + for point in polyline.divide_polyline(discretization) + ] n_nodes = len(nodes) - self.add_node_load(nodes, - x=x/n_nodes if x else x, - y=y/n_nodes if y else y, - z=z/n_nodes if z else z, - xx=xx/n_nodes if xx else xx, - yy=yy/n_nodes if yy else yy, - zz=zz/n_nodes if zz else zz, - axes=axes, name=name, **kwargs) - - def add_planar_area_load(self, polygon, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): - if axes != 'global': - raise NotImplementedError('local axes are not supported yet') + self.add_node_load( + nodes, + x=x / n_nodes if x else x, + y=y / n_nodes if y else y, + z=z / n_nodes if z else z, + xx=xx / n_nodes if xx else xx, + yy=yy / n_nodes if yy else yy, + zz=zz / n_nodes if zz else zz, + axes=axes, + name=name, + **kwargs, + ) + + def add_planar_area_load( + self, polygon, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes="global", name=None, **kwargs + ): + if axes != "global": + raise NotImplementedError("local axes are not supported yet") if not self.problem: - raise ValueError('No problem assigned to the step.') + raise ValueError("No problem assigned to the step.") if not self.model: - raise ValueError('No model assigned to the problem.') + raise ValueError("No model assigned to the problem.") nodes = self.model.find_nodes_in_polygon(polygon)[0] n_nodes = len(nodes) - self.add_node_load(nodes, - x=x/n_nodes if x else x, - y=y/n_nodes if y else y, - z=z/n_nodes if z else z, - xx=xx/n_nodes if xx else xx, - yy=yy/n_nodes if yy else yy, - zz=zz/n_nodes if zz else zz, - axes=axes, name=name, **kwargs) + self.add_node_load( + nodes, + x=x / n_nodes if x else x, + y=y / n_nodes if y else y, + z=z / n_nodes if z else z, + xx=xx / n_nodes if xx else xx, + yy=yy / n_nodes if yy else yy, + zz=zz / n_nodes if zz else zz, + axes=axes, + name=name, + **kwargs, + ) def add_tributary_load(self): raise NotImplementedError @@ -279,10 +349,10 @@ def add_tributary_load(self): # FIXME change to pattern def add_temperature_field(self, field, node): if not isinstance(field, PrescribedTemperatureField): - raise TypeError('{!r} is not a PrescribedTemperatureField.'.format(field)) + raise TypeError("{!r} is not a PrescribedTemperatureField.".format(field)) if not isinstance(node, Node): - raise TypeError('{!r} is not a Node.'.format(node)) + raise TypeError("{!r} is not a Node.".format(node)) node._temperature = field self._fields.setdefault(node.part, {}).setdefault(field, set()).add(node) @@ -294,7 +364,9 @@ def add_temperature_fields(self, field, nodes): # ========================================================================= # Displacements methods # ========================================================================= - def add_displacement(self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes='global', name=None, **kwargs): + def add_displacement( + self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=None, axes="global", name=None, **kwargs + ): """Add a displacement at give nodes to the Step object. Parameters @@ -305,9 +377,10 @@ def add_displacement(self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=N Returns ------- None + """ - if axes != 'global': - raise NotImplementedError('local axes are not supported yet') + if axes != "global": + raise NotImplementedError("local axes are not supported yet") displacement = GeneralDisplacement(x=x, y=y, z=z, xx=xx, yy=yy, zz=zz, axes=axes, name=name, **kwargs) if not isinstance(nodes, Iterable): nodes = [nodes] @@ -315,18 +388,18 @@ def add_displacement(self, nodes, x=None, y=None, z=None, xx=None, yy=None, zz=N class StaticRiksStep(StaticStep): - """Step for use in a static analysis when Riks method is necessary. - - Parameters - ---------- - None - - Attributes - ---------- - None - - """ - - def __init__(self, max_increments=100, initial_inc_size=1, min_inc_size=0.00001, time=1, nlgeom=False, modify=True, name=None, **kwargs): + """Step for use in a static analysis when Riks method is necessary.""" + + def __init__( + self, + max_increments=100, + initial_inc_size=1, + min_inc_size=0.00001, + time=1, + nlgeom=False, + modify=True, + name=None, + **kwargs, + ): super().__init__(max_increments, initial_inc_size, min_inc_size, time, nlgeom, modify, name, **kwargs) raise NotImplementedError diff --git a/src/compas_fea2/problem/steps/step.py b/src/compas_fea2/problem/steps/step.py index e745f5bba..ae12f129d 100644 --- a/src/compas_fea2/problem/steps/step.py +++ b/src/compas_fea2/problem/steps/step.py @@ -1,34 +1,16 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function -from copy import deepcopy -from importlib.metadata import metadata -from json import load -from typing import Type -import compas_fea2 from compas_fea2.base import FEAData -from compas_fea2.model.nodes import Node -from compas_fea2.model.elements import _Element - -from compas_fea2.problem.loads import _Load -from compas_fea2.problem.loads import GravityLoad -from compas_fea2.problem.loads import PointLoad - +from compas_fea2.problem.loads import Load from compas_fea2.problem.patterns import Pattern - from compas_fea2.problem.displacements import GeneralDisplacement - -from compas_fea2.problem.fields import _PrescribedField -from compas_fea2.problem.fields import PrescribedTemperatureField - -from compas_fea2.problem.outputs import _Output +from compas_fea2.problem.fields import PrescribedField from compas_fea2.problem.outputs import FieldOutput from compas_fea2.problem.outputs import HistoryOutput -from compas.geometry import Vector -from compas.geometry import sum_vectors from compas_fea2.utilities._utils import timer import copy @@ -38,21 +20,9 @@ # ============================================================================== -class _Step(FEAData): +class Step(FEAData): """Initialises base Step object. - Note - ---- - Stpes are registered to a :class:`compas_fea2.problem.Problem`. - - Note - ---- - A ``compas_fea2`` analysis is based on the concept of ``steps``, - which represent the sequence in which the state of the model is modified. - Steps can be introduced for example to change the output requests or to change - loads, boundary conditions, analysis procedure, etc. There is no limit on the - number of steps in an analysis. - Parameters ---------- name : str, optional @@ -71,10 +41,20 @@ class _Step(FEAData): results : :class:`compas_fea2.results.StepResults` The results of the analysis at this step + Notes + ----- + Steps are registered to a :class:`compas_fea2.problem.Problem`. + + A ``compas_fea2`` analysis is based on the concept of ``steps``, + which represent the sequence in which the state of the model is modified. + Steps can be introduced for example to change the output requests or to change + loads, boundary conditions, analysis procedure, etc. There is no limit on the + number of steps in an analysis. + """ def __init__(self, name=None, **kwargs): - super(_Step, self).__init__(name=name, **kwargs) + super(Step, self).__init__(name=name, **kwargs) self._field_outputs = set() self._history_outputs = set() self._results = None @@ -109,18 +89,19 @@ def add_output(self, output): Parameters ---------- - output : :class:`compas_fea2.problem._Output` + output : :class:`compas_fea2.problem.Output` The requested output. Returns ------- - :class:`compas_fea2.problem._Output` + :class:`compas_fea2.problem.Output` The requested output. Raises ------ TypeError - if the output is not an instance of an :class:`compas_fea2.problem._Output`. + if the output is not an instance of an :class:`compas_fea2.problem.Output`. + """ output._registration = self if isinstance(output, FieldOutput): @@ -128,13 +109,14 @@ def add_output(self, output): elif isinstance(output, HistoryOutput): self._history_outputs.add(output) else: - raise TypeError('{!r} is not an _Output.'.format(output)) + raise TypeError("{!r} is not an Output.".format(output)) return output # ========================================================================== # Results methods # ========================================================================== - @timer(message='Step results copied in the model in ') + + @timer(message="Step results copied in the model in ") def _store_results_in_model(self, fields=None): """Copy the results for the step in the model object at the nodal and element level. @@ -149,53 +131,54 @@ def _store_results_in_model(self, fields=None): None """ - from compas_fea2.results.sql_wrapper import create_connection_sqlite3, get_database_table, get_all_field_results - import sqlalchemy as db - - engine, connection, metadata = create_connection_sqlite3(self.problem.path_results) - FIELDS = get_database_table(engine, metadata, 'fiedls') - if not fields: - field_column = FIELDS.query.all() - fields=[field for field in field_column.field] - - for field in fields: - field_table = get_database_table(engine, metadata, field) - _, results = get_all_field_results(engine, metadata, field, field_table) - for row in results: - part = self.problem.model.find_part_by_name(row[0]) - if row[2] == 'NODAL': - node_element = part.find_node_by_key(row[3]) - else: - raise NotImplementedError('elements not supported yet') - - node_element._results.setdefault(self.problem, {})[self] = res_field - - - step_results = results[self.name] - # Get part results - for part_name, part_results in step_results.items(): - # Get node/element results - for result_type, nodes_elements_results in part_results.items(): - if result_type not in ['nodes', 'elements']: - continue - # nodes_elements = getattr(self.model.find_part_by_name(part_name, casefold=True), result_type) - func = getattr(self.model.find_part_by_name(part_name, casefold=True), - 'find_{}_by_key'.format(result_type[:-1])) - # Get field results - for key, res_field in nodes_elements_results.items(): - node_element = func(key) - if not node_element: - continue - if fields and not res_field in fields: - continue - node_element._results.setdefault(self.problem, {})[self] = res_field + raise NotImplementedError + # from compas_fea2.results.sql_wrapper import create_connection_sqlite3, get_database_table, get_all_field_results + + # engine, connection, metadata = create_connection_sqlite3(self.problem.path_results) + # FIELDS = get_database_table(engine, metadata, "fields") + # if not fields: + # field_column = FIELDS.query.all() + # fields = [field for field in field_column.field] + + # for field in fields: + # field_table = get_database_table(engine, metadata, field) + # _, results = get_all_field_results(engine, metadata, field, field_table) + # for row in results: + # part = self.problem.model.find_part_by_name(row[0]) + # if row[2] == "NODAL": + # node_element = part.find_node_by_key(row[3]) + # else: + # raise NotImplementedError("elements not supported yet") + + # node_element._results.setdefault(self.problem, {})[self] = res_field + + # step_results = results[self.name] + # # Get part results + # for part_name, part_results in step_results.items(): + # # Get node/element results + # for result_type, nodes_elements_results in part_results.items(): + # if result_type not in ["nodes", "elements"]: + # continue + # # nodes_elements = getattr(self.model.find_part_by_name(part_name, casefold=True), result_type) + # func = getattr( + # self.model.find_part_by_name(part_name, casefold=True), "find_{}_by_key".format(result_type[:-1]) + # ) + # # Get field results + # for key, res_field in nodes_elements_results.items(): + # node_element = func(key) + # if not node_element: + # continue + # if fields and res_field not in fields: + # continue + # node_element._results.setdefault(self.problem, {})[self] = res_field + # ============================================================================== # General Steps # ============================================================================== -class _GeneralStep(_Step): +class GeneralStep(Step): """General Step object for use as a base class in a general static, dynamic or multiphysics analysis. @@ -258,10 +241,22 @@ class _GeneralStep(_Step): Dictionary of the loads assigned to each part in the model in the step. fields : dict Dictionary of the prescribed fields assigned to each part in the model in the step. + """ - def __init__(self, max_increments, initial_inc_size, min_inc_size, time, nlgeom=False, modify=False, restart=False, name=None, **kwargs): - super(_GeneralStep, self).__init__(name=name, **kwargs) + def __init__( + self, + max_increments, + initial_inc_size, + min_inc_size, + time, + nlgeom=False, + modify=False, + restart=False, + name=None, + **kwargs + ): + super(GeneralStep, self).__init__(name=name, **kwargs) self._max_increments = max_increments self._initial_inc_size = initial_inc_size @@ -275,13 +270,13 @@ def __init__(self, max_increments, initial_inc_size, min_inc_size, time, nlgeom= def __rmul__(self, other): if not isinstance(other, (float, int)): - raise TypeError('Step multiplication only allowed with real numbers') + raise TypeError("Step multiplication only allowed with real numbers") step_copy = copy.copy(self) step_copy._patterns = set() for pattern in self._patterns: pattern_copy = copy.copy(pattern) load_copy = copy.copy(pattern.load) - pattern_copy._load = other*load_copy + pattern_copy._load = other * load_copy step_copy._add_pattern(pattern_copy) return step_copy @@ -291,11 +286,11 @@ def displacements(self): @property def loads(self): - return list(filter(lambda p: isinstance(p.load, _Load), self._patterns)) + return list(filter(lambda p: isinstance(p.load, Load), self._patterns)) @property def fields(self): - return list(filter(lambda p: isinstance(p.load, _PrescribedField), self._patterns)) + return list(filter(lambda p: isinstance(p.load, PrescribedField), self._patterns)) @property def max_increments(self): @@ -334,34 +329,34 @@ def restart(self, value): # ========================================================================= def _add_pattern(self, load_pattern): - # type: (_Load, Node | _Element) -> _Load """Add a general load pattern to the Step object. - Warning - ------- - The *load* and the *keys* must be consistent (you should not assing a - line load to a node). Consider using specific methods to assign load, - such as ``add_point_load``, ``add_line_load``, etc. - Parameters ---------- load : obj - any ``compas_fea2`` :class:`compas_fea2.problem._Load` subclass object + any ``compas_fea2`` :class:`compas_fea2.problem.Load` subclass object location : var Location where the load is applied Returns ------- None + + Warnings + -------- + The *load* and the *keys* must be consistent (you should not assing a + line load to a node). Consider using specific methods to assign load, + such as ``add_point_load``, ``add_line_load``, etc. + """ if not isinstance(load_pattern, Pattern): - raise TypeError('{!r} is not a LoadPattern.'.format(load_pattern)) + raise TypeError("{!r} is not a LoadPattern.".format(load_pattern)) if self.problem: if self.model: if not list(load_pattern.distribution).pop().model == self.model: - raise ValueError('The load pattern is not applied to a valid reagion of {!r}'.format(self.model)) + raise ValueError("The load pattern is not applied to a valid reagion of {!r}".format(self.model)) # store location in step self._patterns.add(load_pattern) @@ -370,19 +365,19 @@ def _add_pattern(self, load_pattern): return load_pattern def _add_patterns(self, load_patterns): - # type: (_Load, Node | _Element) -> list(_Load) """Add a load to multiple locations. Parameters ---------- - load : :class:`_Load` + load : :class:`Load` Load to assign to the node location : [var] Locations where the load is applied Returns ------- - load : [:class:`_Load`] + load : [:class:`Load`] Load to assign to the node + """ return [self.add_pattern(load_pattern) for load_pattern in load_patterns] diff --git a/src/compas_fea2/problem/steps_combinations.py b/src/compas_fea2/problem/steps_combinations.py index 82cb3c5f2..6d507e7b1 100644 --- a/src/compas_fea2/problem/steps_combinations.py +++ b/src/compas_fea2/problem/steps_combinations.py @@ -3,25 +3,25 @@ from __future__ import print_function from compas_fea2.base import FEAData -from compas_fea2.problem import Pattern class StepsCombination(FEAData): """A StepsCombination `sums` the analysis results of given steps (:class:`compas_fea2.problem.LoadPattern`). - Note - ---- + Parameters + ---------- + FEAData : _type_ + _description_ + + Notes + ----- By default every analysis in `compas_fea2` is meant to be `non-linear`, in the sense that the effects of a load pattern (:class:`compas_fea2.problem.Pattern`) in a given steps are used as a starting point for the application of the load patterns in the next step. Therefore, the sequence of the steps can affect the results (if the response is actully non-linear). - Parameters - ---------- - FEAData : _type_ - _description_ """ def __init__(self, name=None, **kwargs): diff --git a/src/compas_fea2/results/__init__.py b/src/compas_fea2/results/__init__.py index 4f595642d..64a8a38fe 100644 --- a/src/compas_fea2/results/__init__.py +++ b/src/compas_fea2/results/__init__.py @@ -1,27 +1,12 @@ -""" -******************************************************************************** -results -******************************************************************************** - -.. currentmodule:: compas_fea2.results - -.. autosummary:: - :toctree: generated/ - - Results - NodeFieldResults - -""" from __future__ import absolute_import from __future__ import division from __future__ import print_function from .results import Results, NodeFieldResults -from .sql_wrapper import (create_connection_sqlite3, - get_database_table, - ) -__all__ = [ - 'Results', - 'NodeFieldResults' -] +# from .sql_wrapper import ( +# create_connection_sqlite3, +# get_database_table, +# ) + +__all__ = ["Results", "NodeFieldResults"] diff --git a/src/compas_fea2/results/results.py b/src/compas_fea2/results/results.py index a03ea594e..062019736 100644 --- a/src/compas_fea2/results/results.py +++ b/src/compas_fea2/results/results.py @@ -4,10 +4,8 @@ from typing import Iterable -from compas_fea2.base import FEAData - from compas.geometry import Vector -from compas.geometry import sum_vectors +from compas_fea2.base import FEAData from .sql_wrapper import get_field_results, get_field_labels, get_database_table, create_connection @@ -16,15 +14,16 @@ class Results(FEAData): """Results object. This ensures that the results from all the backends are consistent. - Note - ---- - Results are registered to a :class:`compas_fea2.problem.Problem`. - Parameters ---------- location : var location of the result value : var + + Notes + ----- + Results are registered to a :class:`compas_fea2.problem.Problem`. + """ def __init__(self, location, components, invariants, name=None, **kwargs): @@ -47,29 +46,25 @@ def location(self): @property def vector(self): - if len(self.components)==3: + if len(self.components) == 3: return Vector(*list(self.components.values())) @property def value(self): return self.vector.length - def to_file(self, *args, **kwargs): raise NotImplementedError("this function is not available for the selected backend") + class FieldResults(FEAData): def __init__(self, field_name, step, name=None, *args, **kwargs): super(FieldResults, self).__init__(name, *args, **kwargs) self._registration = step self._db_connection = create_connection(self.problem.path_db) self._field_name = field_name - self._components = get_field_labels(*self.db_connection, - self.field_name, - 'components') - self._invariants = get_field_labels(*self.db_connection, - self.field_name, - 'invariants') + self._components = get_field_labels(*self.db_connection, self.field_name, "components") + self._invariants = get_field_labels(*self.db_connection, self.field_name, "invariants") @property def step(self): @@ -91,7 +86,6 @@ def db_connection(self): def db_connection(self, path_db): self._db_connection = create_connection(path_db) - def _get_field_results(self, field): """_summary_ @@ -135,7 +129,7 @@ class NodeFieldResults(FieldResults): def __init__(self, field_name, step, name=None, *args, **kwargs): super(NodeFieldResults, self).__init__(field_name, step, name, *args, **kwargs) self._results = self._link_field_results_to_model(self._get_field_results(field=self.field_name)[1]) - if len(self.results)!=len(self.model.nodes): + if len(self.results) != len(self.model.nodes): raise ValueError('The requested field is not defined at the nodes. Try "show_elements_field" instead".') self._max_components = {c: self._get_limit("MAX", component=c)[0] for c in self._components} self._min_components = {c: self._get_limit("MIN", component=c)[0] for c in self._components} @@ -160,10 +154,11 @@ def results(self): @property def max(self): - return self._max_invariants['magnitude'][0] + return self._max_invariants["magnitude"][0] + @property def min(self): - return self._min_invariants['magnitude'][0] + return self._min_invariants["magnitude"][0] def _link_field_results_to_model(self, field_results): """Converts the values of the results string to actual nodes of the @@ -197,15 +192,15 @@ def _link_field_results_to_model(self, field_results): print("Part {} not found in model".format(row[0])) continue result = Results( - location=part.find_node_by_key(row[2]), - components={col_names[i]: row[i] for i in range(3, len(self.components)+3)}, - invariants={col_names[i]: row[i] for i in range(len(self.components)+3, len(row))} + location=part.find_node_by_key(row[2]), + components={col_names[i]: row[i] for i in range(3, len(self.components) + 3)}, + invariants={col_names[i]: row[i] for i in range(len(self.components) + 3, len(row))}, ) results.append(result) return results def _get_limit(self, limit="MAX", component="magnitude"): - if component not in self.components+self.invariants: + if component not in self.components + self.invariants: raise ValueError( "The specified component is not valid. Choose from {}".format(self._components + self.invariants) ) @@ -224,10 +219,11 @@ def get_value_at_nodes(self, nodes): steps : _type_, optional _description_, by default None - Return - ------ + Returns + ------- dict Dictionary with {'part':..; 'node':..; 'vector':...} + """ if not isinstance(nodes, Iterable): nodes = [nodes] @@ -264,10 +260,11 @@ def get_value_at_point(self, point, distance, plane=None, steps=None, group_by=[ steps : _type_, optional _description_, by default None - Return - ------ + Returns + ------- dict Dictionary with {'part':..; 'node':..; 'vector':...} + """ steps = [self.step] node = self.model.find_node_by_location(point, distance, plane=None) diff --git a/src/compas_fea2/results/sql_wrapper.py b/src/compas_fea2/results/sql_wrapper.py index 53b6a111c..29c54eefb 100644 --- a/src/compas_fea2/results/sql_wrapper.py +++ b/src/compas_fea2/results/sql_wrapper.py @@ -1,13 +1,10 @@ -from operator import index import sqlalchemy as db - -from math import sqrt -import os import sqlite3 from sqlite3 import Error # TODO convert to sqlalchemy + def create_connection_sqlite3(db_file=None): """Create a database connection to the SQLite database specified by db_file. @@ -17,14 +14,15 @@ def create_connection_sqlite3(db_file=None): Path to the .db file, by default 'None'. If not provided, the database is run in memory. - Return - ------ + Returns + ------- :class:`sqlite3.Connection` | None Connection object or None + """ conn = None try: - conn = sqlite3.connect(db_file or ':memory:') + conn = sqlite3.connect(db_file or ":memory:") except Error as e: print(e) return conn @@ -40,9 +38,10 @@ def _create_table_sqlite3(conn, sql): create_table_sql : str A CREATE TABLE statement - Return - ------ + Returns + ------- None + """ try: c = conn.cursor() @@ -50,6 +49,7 @@ def _create_table_sqlite3(conn, sql): except Error as e: print(e) + def _insert_entry__sqlite3(conn, sql): """General code to insert an entry in a table @@ -60,9 +60,10 @@ def _insert_entry__sqlite3(conn, sql): sql : _type_ _description_ - Return - ------ + Returns + ------- lastrowid + """ try: c = conn.cursor() @@ -73,24 +74,27 @@ def _insert_entry__sqlite3(conn, sql): exit() return c.lastrowid + def create_field_description_table_sqlite3(conn): - """ Create the table containing general results information and field + """Create the table containing general results information and field descriptions. Parameters ---------- conn : - Return - ------ + Returns + ------- None + """ with conn: sql = """CREATE TABLE IF NOT EXISTS fields (field text, description text, components text, invariants text, UNIQUE(field) );""" _create_table_sqlite3(conn, sql) + def insert_field_description_sqlite3(conn, field, description, components_names, invariants_names): - """ Create the table containing general results information and field + """Create the table containing general results information and field descriptions. Parameters @@ -104,18 +108,21 @@ def insert_field_description_sqlite3(conn, field, description, components_names, invariants_names : Iterable Output field invariants names. - Return - ------ + Returns + ------- None + """ - sql = """ INSERT OR IGNORE INTO fields VALUES ('{}', '{}', '{}', '{}')""".format(field, - description, - components_names, - invariants_names, - ) + sql = """ INSERT OR IGNORE INTO fields VALUES ('{}', '{}', '{}', '{}')""".format( + field, + description, + components_names, + invariants_names, + ) return _insert_entry__sqlite3(conn, sql) + def create_field_table_sqlite3(conn, field, components_names): """Create the results table for the given field. @@ -130,14 +137,16 @@ def create_field_table_sqlite3(conn, field, components_names): invariants_names : Iterable Output field invariants names. - Return - ------ + Returns + ------- None + """ # FOREIGN KEY (step) REFERENCES analysis_results (step_name), with conn: sql = """CREATE TABLE IF NOT EXISTS {} (step text, part text, type text, position text, key integer, {});""".format( - field, ', '.join(['{} float'.format(c) for c in components_names])) + field, ", ".join(["{} float".format(c) for c in components_names]) + ) _create_table_sqlite3(conn, sql) @@ -153,21 +162,17 @@ def insert_field_results_sqlite3(conn, field, node_results_data): node_results_data : Iterable Output field components values. - Return - ------ + Returns + ------- int Index of the inserted item. + """ - sql = """ INSERT INTO {} VALUES ({})""".format(field, - ', '.join( - ["'"+str(c)+"'" for c in node_results_data]) - ) + sql = """ INSERT INTO {} VALUES ({})""".format(field, ", ".join(["'" + str(c) + "'" for c in node_results_data])) return _insert_entry__sqlite3(conn, sql) - - def create_connection(db_file=None): """Create a database connection to the SQLite database specified by db_file. @@ -177,16 +182,18 @@ def create_connection(db_file=None): Path to the .db file, by default 'None'. If not provided, the database is run in memory. - Return - ------ + Returns + ------- :class:`sqlite3.Connection` | None Connection object or None + """ engine = db.create_engine("sqlite:///{}".format(db_file)) connection = engine.connect() metadata = db.MetaData() return engine, connection, metadata + def get_database_table(engine, metadata, table_name): """Retrieve a table from the database. @@ -206,6 +213,7 @@ def get_database_table(engine, metadata, table_name): """ return db.Table(table_name, metadata, autoload=True, autoload_with=engine) + def get_query_results(connection, table, columns, test): """Get the filtering query to execute. @@ -230,6 +238,7 @@ def get_query_results(connection, table, columns, test): ResultSet = ResultProxy.fetchall() return ResultProxy, ResultSet + def get_field_labels(engine, connection, metadata, field, label): """Get the names of the components or invariants of the field @@ -251,40 +260,40 @@ def get_field_labels(engine, connection, metadata, field, label): _type_ _description_ """ - FIELDS = get_database_table(engine, metadata, 'fields') + FIELDS = get_database_table(engine, metadata, "fields") query = db.select([FIELDS.columns[label]]).where(FIELDS.columns.field == field) ResultProxy = connection.execute(query) ResultSet = ResultProxy.fetchall() - return ResultSet[0][0].split(' ') + return ResultSet[0][0].split(" ") + def get_all_field_results(engine, connection, metadata, table): - components = get_field_labels(engine, connection, metadata, str(table), 'components') - invariants = get_field_labels(engine, connection, metadata, str(table), 'invariants') - columns = ['part', 'position', 'key']+components+invariants + components = get_field_labels(engine, connection, metadata, str(table), "components") + invariants = get_field_labels(engine, connection, metadata, str(table), "invariants") + columns = ["part", "position", "key"] + components + invariants query = db.select([table.columns[column] for column in columns]) ResultProxy = connection.execute(query) ResultSet = ResultProxy.fetchall() return ResultProxy, ResultSet + def get_field_results(engine, connection, metadata, table, test): - components = get_field_labels(engine, connection, metadata, str(table), 'components') - invariants = get_field_labels(engine, connection, metadata, str(table), 'invariants') - labels = ['part', 'position', 'key']+components+invariants - ResultProxy, ResultSet = get_query_results(connection, - table, - labels, - test) + components = get_field_labels(engine, connection, metadata, str(table), "components") + invariants = get_field_labels(engine, connection, metadata, str(table), "invariants") + labels = ["part", "position", "key"] + components + invariants + ResultProxy, ResultSet = get_query_results(connection, table, labels, test) return ResultProxy, (labels, ResultSet) -if __name__ == '__main__': - import os +if __name__ == "__main__": from pprint import pprint + engine, connection, metadata = create_connection_sqlite3( - r'C:\Code\myRepos\swissdemo\data\q_5\output\1_0\ULS\ULS-results.db') + r"C:\Code\myRepos\swissdemo\data\q_5\output\1_0\ULS\ULS-results.db" + ) # U = db.Table('U', metadata, autoload=True, autoload_with=engine) - U = get_database_table(engine, metadata, 'U') + U = get_database_table(engine, metadata, "U") # print(RF.columns.keys()) # query = db.select([U]).where(U.columns.key == 0) # query = db.select([U]).where(U.columns.part.in_ == ['BLOCK_0', 'TIE_21']) diff --git a/src/compas_fea2/units/__init__.py b/src/compas_fea2/units/__init__.py index 2957ba85a..ee8fc0be5 100644 --- a/src/compas_fea2/units/__init__.py +++ b/src/compas_fea2/units/__init__.py @@ -1,17 +1,10 @@ -""" -******************************************************************************** -Units -******************************************************************************** - -compas_fe2 can use Pint for units consistency. - -""" - import os from pint import UnitRegistry + HERE = os.path.dirname(__file__) # U.define('@alias pascal = Pa') -def units(system='SI'): - return UnitRegistry(os.path.join(HERE, 'fea2_en.txt'), system=system) + +def units(system="SI"): + return UnitRegistry(os.path.join(HERE, "fea2_en.txt"), system=system) diff --git a/src/compas_fea2/utilities/__init__.py b/src/compas_fea2/utilities/__init__.py index 528baa0c7..e11cace39 100644 --- a/src/compas_fea2/utilities/__init__.py +++ b/src/compas_fea2/utilities/__init__.py @@ -1,32 +1,3 @@ -""" -******************************************************************************** -Utilities -******************************************************************************** - -.. currentmodule:: compas_fea2.utilities - - -Functions -========= - -.. autosummary:: - :toctree: generated/ - - colorbar - combine_all_sets - group_keys_by_attribute - group_keys_by_attributes - identify_ranges - mesh_from_shell_elements - network_order - normalise_data - principal_stresses - process_data - postprocess - plotvoxels - - -""" from __future__ import absolute_import from __future__ import division from __future__ import print_function @@ -43,20 +14,20 @@ principal_stresses, process_data, postprocess, - plotvoxels + plotvoxels, ) __all__ = [ - 'colorbar', - 'combine_all_sets', - 'group_keys_by_attribute', - 'group_keys_by_attributes', - 'identify_ranges', - 'mesh_from_shell_elements', - 'network_order', - 'normalise_data', - 'principal_stresses', - 'process_data', - 'postprocess', - 'plotvoxels' + "colorbar", + "combine_all_sets", + "group_keys_by_attribute", + "group_keys_by_attributes", + "identify_ranges", + "mesh_from_shell_elements", + "network_order", + "normalise_data", + "principal_stresses", + "process_data", + "postprocess", + "plotvoxels", ] diff --git a/src/compas_fea2/utilities/_utils.py b/src/compas_fea2/utilities/_utils.py index 764da2f6b..2e643b530 100644 --- a/src/compas_fea2/utilities/_utils.py +++ b/src/compas_fea2/utilities/_utils.py @@ -3,7 +3,6 @@ from __future__ import print_function import os -import inspect from subprocess import Popen from subprocess import PIPE @@ -12,24 +11,24 @@ from compas.geometry import bounding_box from compas.geometry import Point, Box -import importlib import itertools from typing import Iterable - def timer(_func=None, *, message=None): """Print the runtime of the decorated function""" + def decorator_timer(func): @wraps(func) def wrapper_timer(*args, **kwargs): - start_time = perf_counter() # 1 + start_time = perf_counter() # 1 value = func(*args, **kwargs) - end_time = perf_counter() # 2 - run_time = end_time - start_time # 3 - m = message or 'Finished {!r} in'.format(func.__name__) - print('{} {:.4f} secs'.format(m, run_time)) + end_time = perf_counter() # 2 + run_time = end_time - start_time # 3 + m = message or "Finished {!r} in".format(func.__name__) + print("{} {:.4f} secs".format(m, run_time)) return value + return wrapper_timer if _func is None: @@ -92,17 +91,21 @@ def get_docstring(cls): """ Decorator: Append to a function's docstring. """ + def _decorator(func): - func_name = func.__qualname__.split('.')[-1] - doc_parts = getattr(cls, func_name).__doc__.split('Returns') + func_name = func.__qualname__.split(".")[-1] + doc_parts = getattr(cls, func_name).__doc__.split("Returns") note = """ Returns ------- list of {} - """.format(doc_parts[1].split('-------\n')[1]) + """.format( + doc_parts[1].split("-------\n")[1] + ) func.__doc__ = doc_parts[0] + note return func + return _decorator @@ -123,26 +126,27 @@ def part_method(f): @wraps(f) def wrapper(*args, **kwargs): - func_name = f.__qualname__.split('.')[-1] + func_name = f.__qualname__.split(".")[-1] self_obj = args[0] - if kwargs.get('dict_format', None): + if kwargs.get("dict_format", None): return {part: vars for part in self_obj.parts if (vars := getattr(part, func_name)(*args[1::], **kwargs))} else: res = [vars for part in self_obj.parts if (vars := getattr(part, func_name)(*args[1::], **kwargs))] - if kwargs.get('merge', None): + if kwargs.get("merge", None): list(itertools.chain.from_iterable(res)) return res + # func_name = f.__qualname__.split('.')[-1] # wrapper.__doc__ = getattr(DeformablePart, func_name).__doc__.split('Returns')[0] # wrapper.__doc__ += "ciao" -# docs = getattr(DeformablePart, method).__doc__.split('Returns', 1)[0] -# docs += """ -# Returns -# ------- -# {:class:`compas_fea2.model.DeformablePart`: var} -# dictionary with the results of the method per each part in the model. -# """ + # docs = getattr(DeformablePart, method).__doc__.split('Returns', 1)[0] + # docs += """ + # Returns + # ------- + # {:class:`compas_fea2.model.DeformablePart`: var} + # dictionary with the results of the method per each part in the model. + # """ return wrapper @@ -159,15 +163,16 @@ def step_method(f): Returns ------- - {:class:`compas_fea2.problem._Step`: var} + {:class:`compas_fea2.problem.Step`: var} dictionary with the results of the method per each step in the problem. """ @wraps(f) def wrapper(*args, **kwargs): - func_name = f.__qualname__.split('.')[-1] + func_name = f.__qualname__.split(".")[-1] self_obj = args[0] return {step: vars for step in self_obj.steps if (vars := getattr(step, func_name)(*args[1::], **kwargs))} + return wrapper @@ -190,21 +195,22 @@ def problem_method(f): @wraps(f) def wrapper(*args, **kwargs): - func_name = f.__qualname__.split('.')[-1] + func_name = f.__qualname__.split(".")[-1] self_obj = args[0] - problems = kwargs.setdefault('problems', self_obj.problems) + problems = kwargs.setdefault("problems", self_obj.problems) if not problems: - raise ValueError('No problems found in the model') + raise ValueError("No problems found in the model") if not isinstance(problems, Iterable): problems = [problems] vars = {} for problem in problems: if problem.model != self_obj: - raise ValueError('{} is not registered to this model'.format(problem)) - if 'steps' in kwargs: - kwargs.setdefault('steps', self_obj.steps) + raise ValueError("{} is not registered to this model".format(problem)) + if "steps" in kwargs: + kwargs.setdefault("steps", self_obj.steps) var = getattr(problem, func_name)(*args[1::], **kwargs) if var: vars[problem] = vars return vars + return wrapper diff --git a/src/compas_fea2/utilities/functions.py b/src/compas_fea2/utilities/functions.py index acff182dd..b500b3f9e 100644 --- a/src/compas_fea2/utilities/functions.py +++ b/src/compas_fea2/utilities/functions.py @@ -47,18 +47,18 @@ __all__ = [ - 'colorbar', - 'combine_all_sets', - 'group_keys_by_attribute', - 'group_keys_by_attributes', - 'network_order', - 'normalise_data', - 'postprocess', - 'process_data', - 'principal_stresses', - 'plotvoxels', - 'identify_ranges', - 'mesh_from_shell_elements' + "colorbar", + "combine_all_sets", + "group_keys_by_attribute", + "group_keys_by_attributes", + "network_order", + "normalise_data", + "postprocess", + "process_data", + "principal_stresses", + "plotvoxels", + "identify_ranges", + "mesh_from_shell_elements", ] @@ -66,8 +66,9 @@ # General methods # ========================================================================= + def process_data(data, dtype, iptype, nodal, elements, n): - """ Process the raw data. + """Process the raw data. Parameters ---------- @@ -93,18 +94,16 @@ def process_data(data, dtype, iptype, nodal, elements, n): """ - if dtype == 'nodal': - + if dtype == "nodal": vn = array(data)[:, newaxis] ve = None - elif dtype == 'element': - + elif dtype == "element": m = len(elements) lengths = zeros(m, dtype=int64) data_array = zeros((m, 20), dtype=float64) - iptypes = {'max': 0, 'min': 1, 'mean': 2, 'abs': 3} + iptypes = {"max": 0, "min": 1, "mean": 2, "abs": 3} for ekey, item in data.items(): fdata = list(item.values()) @@ -129,32 +128,28 @@ def process_data(data, dtype, iptype, nodal, elements, n): AT = A.transpose() def _process(data_array, lengths, iptype): - m = len(lengths) ve = zeros((m, 1)) for i in range(m): - if iptype == 0: - ve[i] = max(data_array[i, :lengths[i]]) + ve[i] = max(data_array[i, : lengths[i]]) elif iptype == 1: - ve[i] = min(data_array[i, :lengths[i]]) + ve[i] = min(data_array[i, : lengths[i]]) elif iptype == 2: - ve[i] = mean(data_array[i, :lengths[i]]) + ve[i] = mean(data_array[i, : lengths[i]]) elif iptype == 3: - ve[i] = max(abs(data_array[i, :lengths[i]])) + ve[i] = max(abs(data_array[i, : lengths[i]])) return ve def _nodal(rows, cols, nodal, ve, n): - vn = zeros((n, 1)) for i in range(len(rows)): - node = cols[i] element = rows[i] @@ -170,18 +165,18 @@ def _nodal(rows, cols, nodal, ve, n): ve = _process(data_array, lengths, iptypes[iptype]) - if nodal == 'mean': + if nodal == "mean": vsum = asarray(AT.dot(ve)) vn = vsum / sum(AT, 1) else: - vn = _nodal(rows, cols, 0 if nodal == 'max' else 1, ve, n) + vn = _nodal(rows, cols, 0 if nodal == "max" else 1, ve, n) return vn, ve def identify_ranges(data): - """ Identifies continuous interger series from a list and returns a list of ranges. + """Identifies continuous interger series from a list and returns a list of ranges. Parameters ---------- @@ -209,8 +204,8 @@ def identify_ranges(data): return ranges -def colorbar(fsc, input='array', type=255): - """ Creates RGB color information from -1 to 1 scaled values. +def colorbar(fsc, input="array", type=255): + """Creates RGB color information from -1 to 1 scaled values. Parameters ---------- @@ -232,16 +227,14 @@ def colorbar(fsc, input='array', type=255): g = -abs(fsc - 0.25) * 2 + 1.5 b = -(fsc - 0.25) * 2 - if input == 'array': - + if input == "array": rgb = hstack([r, g, b]) rgb[rgb > 1] = 1 rgb[rgb < 0] = 0 return rgb * type - elif input == 'float': - + elif input == "float": r = max([0, min([1, r])]) g = max([0, min([1, g])]) b = max([0, min([1, b])]) @@ -250,7 +243,7 @@ def colorbar(fsc, input='array', type=255): def mesh_from_shell_elements(structure): - """ Returns a Mesh datastructure object from a Structure's ShellElement objects. + """Returns a Mesh datastructure object from a Structure's ShellElement objects. Parameters ---------- @@ -264,7 +257,7 @@ def mesh_from_shell_elements(structure): """ - ekeys = [ekey for ekey in structure.elements if structure.elements[ekey].__name__ == 'ShellElement'] + ekeys = [ekey for ekey in structure.elements if structure.elements[ekey].__name__ == "ShellElement"] nkeys = {nkey for ekey in ekeys for nkey in structure.elements[ekey].nodes} mesh = Mesh() @@ -279,17 +272,14 @@ def mesh_from_shell_elements(structure): def volmesh_from_solid_elements(structure): - raise NotImplementedError def network_from_line_elements(structure): - raise NotImplementedError def _angle(A, B, C): - AB = B - A BC = C - B th = arccos(sum(AB * BC) / (sqrt(sum(AB**2)) * sqrt(sum(BC**2)))) * 180 / pi @@ -297,7 +287,6 @@ def _angle(A, B, C): def _centre(p1, p2, p3): - ax, ay = p1[0], p1[1] bx, by = p2[0], p2[1] cx, cy = p3[0], p3[1] @@ -310,13 +299,13 @@ def _centre(p1, p2, p3): g = 2 * (a * (cy - by) - b * (cx - bx)) centerx = (d * e - b * f) / g centery = (a * f - c * e) / g - r = sqrt((ax - centerx)**2 + (ay - centery)**2) + r = sqrt((ax - centerx) ** 2 + (ay - centery) ** 2) return [centerx, centery, 0], r def combine_all_sets(sets_a, sets_b): - """ Combines two nested lists of node or element sets into the minimum ammount of set combinations. + """Combines two nested lists of node or element sets into the minimum ammount of set combinations. Parameters ---------- @@ -337,12 +326,12 @@ def combine_all_sets(sets_a, sets_b): for j in sets_b: for x in sets_a[i]: if x in sets_b[j]: - comb.setdefault(str(i) + ',' + str(j), []).append(x) + comb.setdefault(str(i) + "," + str(j), []).append(x) return comb -def group_keys_by_attribute(adict, name, tol='3f'): - """ Make group keys by shared attribute values. +def group_keys_by_attribute(adict, name, tol="3f"): + """Make group keys by shared attribute values. Parameters ---------- @@ -364,14 +353,14 @@ def group_keys_by_attribute(adict, name, tol='3f'): for key, item in adict.items(): if name in item: value = item[name] - if type(value) == float: - value = '{0:.{1}}'.format(value, tol) + if isinstance(value, float): + value = "{0:.{1}}".format(value, tol) groups.setdefault(value, []).append(key) return groups -def group_keys_by_attributes(adict, names, tol='3f'): - """ Make group keys by shared values of attributes. +def group_keys_by_attributes(adict, names, tol="3f"): + """Make group keys by shared values of attributes. Parameters ---------- @@ -395,20 +384,20 @@ def group_keys_by_attributes(adict, names, tol='3f'): for name in names: if name in item: value = item[name] - if type(value) == float: - value = '{0:.{1}}'.format(value, tol) + if isinstance(value, float): + value = "{0:.{1}}".format(value, tol) else: value = str(value) else: - value = '-' + value = "-" values.append(value) - vkey = '_'.join(values) + vkey = "_".join(values) groups.setdefault(vkey, []).append(key) return groups def network_order(start, structure, network): - """ Extract node and element orders from a Network for a given start-point. + """Extract node and element orders from a Network for a given start-point. Parameters ---------- @@ -433,7 +422,7 @@ def network_order(start, structure, network): """ gkey_key = network.gkey_key() - start = gkey_key[geometric_key(start, '{0}f'.format(structure.tol))] + start = gkey_key[geometric_key(start, "{0}f".format(structure.tol))] leaves = network.leaves() leaves.remove(start) end = leaves[0] @@ -452,14 +441,14 @@ def network_order(start, structure, network): xyz_sp = structure.node_xyz(sp) xyz_ep = structure.node_xyz(ep) dL = distance_point_point(xyz_sp, xyz_ep) - arclengths.append(length + dL / 2.) + arclengths.append(length + dL / 2.0) length += dL return nodes, elements, arclengths, length def normalise_data(data, cmin, cmax): - """ Normalise a vector of data to between -1 and 1. + """Normalise a vector of data to between -1 and 1. Parameters ---------- @@ -491,7 +480,7 @@ def normalise_data(data, cmin, cmax): def postprocess(nodes, elements, ux, uy, uz, data, dtype, scale, cbar, ctype, iptype, nodal): - """ Post-process data from analysis results for given step and field. + """Post-process data from analysis results for given step and field. Parameters ---------- @@ -547,11 +536,11 @@ def postprocess(nodes, elements, ux, uy, uz, data, dtype, scale, cbar, ctype, ip vn, ve = process_data(data=data, dtype=dtype, iptype=iptype, nodal=nodal, elements=elements, n=len(U)) fscaled, fabs = normalise_data(data=vn, cmin=cbar[0], cmax=cbar[1]) - NodeBases = colorbar(fsc=fscaled, input='array', type=ctype) + NodeBases = colorbar(fsc=fscaled, input="array", type=ctype) - if dtype == 'element': + if dtype == "element": escaled, eabs = normalise_data(data=ve, cmin=cbar[0], cmax=cbar[1]) - ElementBases = colorbar(fsc=escaled, input='array', type=ctype) + ElementBases = colorbar(fsc=escaled, input="array", type=ctype) ElementBases_ = [list(i) for i in list(ElementBases)] else: eabs = 0 @@ -566,7 +555,7 @@ def postprocess(nodes, elements, ux, uy, uz, data, dtype, scale, cbar, ctype, ip def plotvoxels(values, U, vdx, indexing=None): - """ Plot values as voxel data. + """Plot values as voxel data. Parameters ---------- @@ -597,7 +586,7 @@ def plotvoxels(values, U, vdx, indexing=None): # Zm, Ym, Xm = meshgrid(X, Y, Z, indexing='ij') f = abs(asarray(values)) - Am = squeeze(griddata(U, f, (Xm, Ym, Zm), method='linear', fill_value=0)) + Am = squeeze(griddata(U, f, (Xm, Ym, Zm), method="linear", fill_value=0)) Am[isnan(Am)] = 0 # voxels = VtkViewer(data={'voxels': Am}) @@ -608,7 +597,7 @@ def plotvoxels(values, U, vdx, indexing=None): def principal_stresses(data, ptype, scale, rotate): - """ Performs principal stress calculations. + """Performs principal stress calculations. Parameters ---------- @@ -636,11 +625,11 @@ def principal_stresses(data, ptype, scale, rotate): """ - axes = data['axes'] - s11 = data['sxx'] - s22 = data['syy'] - s12 = data['sxy'] - spr = data['s{0}p'.format(ptype)] + axes = data["axes"] + s11 = data["sxx"] + s22 = data["syy"] + s12 = data["sxy"] + spr = data["s{0}p".format(ptype)] ekeys = spr.keys() m = len(ekeys) @@ -660,14 +649,14 @@ def principal_stresses(data, ptype, scale, rotate): try: e11[i, :] = axes[ekey][0] e22[i, :] = axes[ekey][1] - s11_sp1[i] = s11[ekey]['ip1_sp1'] - s22_sp1[i] = s22[ekey]['ip1_sp1'] - s12_sp1[i] = s12[ekey]['ip1_sp1'] - spr_sp1[i] = spr[ekey]['ip1_sp1'] - s11_sp5[i] = s11[ekey]['ip1_sp5'] - s22_sp5[i] = s22[ekey]['ip1_sp5'] - s12_sp5[i] = s12[ekey]['ip1_sp5'] - spr_sp5[i] = spr[ekey]['ip1_sp5'] + s11_sp1[i] = s11[ekey]["ip1_sp1"] + s22_sp1[i] = s22[ekey]["ip1_sp1"] + s12_sp1[i] = s12[ekey]["ip1_sp1"] + spr_sp1[i] = spr[ekey]["ip1_sp1"] + s11_sp5[i] = s11[ekey]["ip1_sp5"] + s22_sp5[i] = s22[ekey]["ip1_sp5"] + s12_sp5[i] = s12[ekey]["ip1_sp5"] + spr_sp5[i] = spr[ekey]["ip1_sp5"] except Exception: pass diff --git a/src/compas_fea2/utilities/loads.py b/src/compas_fea2/utilities/loads.py index 18ba1b11c..6b4f65a7f 100644 --- a/src/compas_fea2/utilities/loads.py +++ b/src/compas_fea2/utilities/loads.py @@ -1,4 +1,4 @@ -def mesh_points_pattern(model, mesh, t=0.05, side='top'): +def mesh_points_pattern(model, mesh, t=0.05, side="top"): """Find all the nodes of a model vertically (z) aligned with the vertices of a given mesh. Parameters @@ -24,12 +24,11 @@ def mesh_points_pattern(model, mesh, t=0.05, side='top'): for vertex in mesh.vertices(): point = mesh.vertex_coordinates(vertex) tributary_area = mesh.vertex_area(vertex) - for part in model.parts: #filter(lambda p: 'block' in p.name, model.parts): - nodes = part.find_nodes_where( - [f'{point[0]-t} <= x <= {point[0]+t}', f'{point[1]-t} <= y <= {point[1]+t}']) + for part in model.parts: # filter(lambda p: 'block' in p.name, model.parts): + nodes = part.find_nodes_where([f"{point[0]-t} <= x <= {point[0]+t}", f"{point[1]-t} <= y <= {point[1]+t}"]) if nodes: - if side == 'top': - pattern.setdefault(vertex, {})['area'] = tributary_area - pattern[vertex].setdefault('nodes', []).append(list(sorted(nodes, key=lambda n: n.z))[-1]) + if side == "top": + pattern.setdefault(vertex, {})["area"] = tributary_area + pattern[vertex].setdefault("nodes", []).append(list(sorted(nodes, key=lambda n: n.z))[-1]) # TODO add additional sides return pattern diff --git a/tasks.py b/tasks.py index e1531bdea..d1bbbc512 100644 --- a/tasks.py +++ b/tasks.py @@ -17,17 +17,14 @@ docs.linkcheck, tests.test, tests.testdocs, - build.build_ghuser_components, + tests.testcodeblocks, build.prepare_changelog, build.clean, build.release, + build.build_ghuser_components, ) ns.configure( { "base_folder": os.path.dirname(__file__), - "ghuser": { - "source_dir": "src/compas_fea2/ghpython/components", - "target_dir": "src/compas_fea2/ghpython/components/ghuser", - }, } ) diff --git a/tests/compas_fea2/backends/PLACEHOLDER b/tests/compas_fea2/backends/PLACEHOLDER deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/compas_fea2/backends/abaqus/test_materials.py b/tests/compas_fea2/backends/abaqus/test_materials.py deleted file mode 100644 index 005c9050b..000000000 --- a/tests/compas_fea2/backends/abaqus/test_materials.py +++ /dev/null @@ -1,173 +0,0 @@ -import pytest -import compas_fea2 - -from compas_fea2.model import Concrete - -from compas_fea2.model.materials import Concrete, ElasticIsotropic, Stiff, UserMaterial - -young_mod = 1000 -poisson_ratio = 0.3 -density = 1.0 -name = "test" -unilateral = ["nc", "nt", "junk"] - -# ============================================================================== -# Tests - Elastic Isotropic -# ============================================================================== - - -def test_ElasticIsotropic_generate_data_1(): - material_input = ElasticIsotropic(name, young_mod, poisson_ratio, density, - unilateral=unilateral[0]) - function_out = material_input._generate_jobdata() - expected_out = ("*Material, name={}\n" - "*Density\n" - "{},\n" - "*Elastic\n" - "{}, {}\n" - "*NO COMPRESSION\n").format(name, density, young_mod, poisson_ratio) - assert function_out == expected_out - - -def test_ElasticIsotropic_generate_data_2(): - material_input = ElasticIsotropic(name, young_mod, poisson_ratio, density, - unilateral=unilateral[1]) - function_out = material_input._generate_jobdata() - expected_out = ("*Material, name={}\n" - "*Density\n" - "{},\n" - "*Elastic\n" - "{}, {}\n" - "*NO TENSION\n").format(name, density, young_mod, poisson_ratio) - assert function_out == expected_out - - -def test_ElasticIsotropic_generate_data_3(): - # FIXME: don't leave this in the future. It's here only to show an example of a badly designed test. - material_input = ElasticIsotropic(name, young_mod, poisson_ratio, density, - unilateral=unilateral[0]) - function_out = material_input._generate_jobdata() - expected_out = ("*Material, name={}\n" - "*Density\n" - "{},\n" - "*Elastic\n" - "{}, {}\n" - "*NO TENSION\n").format(name, density, young_mod, poisson_ratio) - assert function_out != expected_out - - -def test_ElasticIsotropic_generate_data_4(): - with pytest.raises(Exception) as exc_info: - ElasticIsotropic(name, young_mod, poisson_ratio, density, unilateral=unilateral[2]) - - assert exc_info.value.args[0] == ("keyword {} for unilateral parameter not recognised. " - "Please review the documentation").format(unilateral[2]) - - -# ============================================================================== -# Tests - Stiff -# ============================================================================== - -def test_Stiff_generate_data(): - # Get the default values for p and v - init_stiff = Stiff("def", 100) - # My material - material_input = Stiff(name, young_mod) - function_out = material_input._generate_jobdata() - expected_out = ("*Material, name={}\n" - "*Density\n" - "{},\n" - "*Elastic\n" - "{}, {}\n").format(name, init_stiff.p, young_mod, init_stiff.v['v']) - assert function_out == expected_out - - -# ============================================================================== -# Tests - Steel -# ============================================================================== - -def test_Steel_generate_data(): - E = 1000.0 - fy = 400.0 - fu = 500.0 - eu = 30.0 - # Done in Steel constructor - ep = eu * 1E-2 - (fy * 1E6) / (E * 1E9) - # My material - material_input = Steel("steely", fy, fu, eu, E, 0.3, 1.0) - function_out = material_input._generate_jobdata() - # Expected - # Materials are units dependent - (E, f) - expected_out = ("*Material, name={}\n" - "*Density\n" - "{},\n" - "*Elastic\n" - "{}, {}\n" - "*Plastic\n" - "{}, {}\n" - "{}, {}").format("steely", 1.0, E*1E9, 0.3, fy*1E6, 0, fu*1E6, ep) - assert function_out == expected_out - - -# ============================================================================== -# Tests - Concrete -# ============================================================================== - -def test_Concrete_generate_data(): - # name = "concrety" - # fck = 1000.0 - # v = poisson_ratio - # p = 1.0 - # fr = [2.5, 1.0] - # material_base = Concrete(name, fck, v, p, fr) - # # My material - # material_input = Concrete(name, fck, v, p, fr) - # function_out = material_input._generate_jobdata() - # print(function_out) - # # Expected - # # Materials are units dependent - (E, f) - # TODO: to complicated to test yet - pass - -# ============================================================================== -# Tests - UserMaterial -# ============================================================================== - - -def test_UserMaterial_constructor_1(): - """ Without any constants """ - name = 'user' - sub_path = '/user/project/my_material' - p = 2.0 - material_input = UserMaterial('user', sub_path, p) - - assert material_input.get_constants() == [[], material_input.data] - - -def test_UserMaterial_constructor_2(): - """ - Without a few constants. - """ - name = 'user' - sub_path = '/user/project/my_material' - p = 2.0 - extra_args = dict(t=100, - a="xyz", - c=-30.5, - key=1) - material_input = UserMaterial('user', sub_path, p, **extra_args) - - # Test _generate_jobdata() - - expected_res = '*Material, name={}\n*Density\n{},'\ - '\n*User Material, constants={}\n{}, xyz, {}'.format(name, p, - len(extra_args), - extra_args['key'], - extra_args['c'], - extra_args['a'], - extra_args['t']) - - # Test get_constants() - - expected_res = [*extra_args.values(), [*extra_args.values()], material_input.jobdata] - assert material_input.get_constants() == expected_res diff --git a/tests/compas_fea2/interfaces/PLACEHOLDER b/tests/compas_fea2/interfaces/PLACEHOLDER deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/compas_fea2/utilities/PLACEHOLDER b/tests/compas_fea2/utilities/PLACEHOLDER deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/test_placeholder.py b/tests/test_placeholder.py index 601241aae..3ada1ee4e 100644 --- a/tests/test_placeholder.py +++ b/tests/test_placeholder.py @@ -1,2 +1,2 @@ def test_placeholder(): - assert True \ No newline at end of file + assert True