From e135304dc343687c1affad027536db8528455794 Mon Sep 17 00:00:00 2001 From: krande Date: Sat, 30 Oct 2021 20:42:30 +0200 Subject: [PATCH] Add verification report for adapy in ci/cd pipeline --- .github/workflows/ci-FEM.yml | 13 +- .github/workflows/ci.yml | 4 +- images/tests/build_verification_report.py | 106 +++++++++++ images/tests/common.py | 168 ++++++++++++++++++ images/tests/report/00-main/00-intro.md | 47 +++++ .../report/00-main/01-eigenvalue-summary.md | 45 +++++ .../report/00-main/02-nonlinear-summary.md | 8 + .../report/01-app/00-results-detailed.md | 38 ++++ images/tests/report/metadata.yaml | 5 + images/tests/test_fem_eig_cantilever.py | 32 ++-- 10 files changed, 443 insertions(+), 23 deletions(-) create mode 100644 images/tests/build_verification_report.py create mode 100644 images/tests/common.py create mode 100644 images/tests/report/00-main/00-intro.md create mode 100644 images/tests/report/00-main/01-eigenvalue-summary.md create mode 100644 images/tests/report/00-main/02-nonlinear-summary.md create mode 100644 images/tests/report/01-app/00-results-detailed.md create mode 100644 images/tests/report/metadata.yaml diff --git a/.github/workflows/ci-FEM.yml b/.github/workflows/ci-FEM.yml index a480e6780..c599d5b12 100644 --- a/.github/workflows/ci-FEM.yml +++ b/.github/workflows/ci-FEM.yml @@ -33,4 +33,15 @@ jobs: - name: Build run: docker build -t ada/testing . - name: Test - run: docker run ada/testing bash -c "pip install pytest pydantic && cd /home/tests/fem && pytest" + run: docker run ada/testing bash -c "pip install pytest pandas pydantic && cd /home/tests/fem && pytest" + - name: Build Verification Report + run: cd images/tests && python build_verification_report.py + - name: Upload Zip file to release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: images/tests/temp/_dist/ADA-FEA-verification.docx + asset_name: ADA-FEA-verification-${{steps.date.outputs.date}}.docx + tag: "ADA-FEA-verification-${{steps.date.outputs.date}}" + overwrite: true + body: "Verification Report for ADAPY - ${{steps.date.outputs.date}}" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9aa9d149..378ab8527 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,13 +108,13 @@ jobs: if: ${{ github.event.workflow_run.conclusion == 'success' }} run: echo 'Hello world' - name: upload to conda -c krande - if: github.event_name == 'push' && github.ref == 'refs/heads/main' && ${{ github.event.workflow_run.conclusion == 'success' }} + if: github.event_name == 'push' && github.ref == 'refs/heads/main' && github.event.workflow_run.conclusion == 'success' shell: bash -l {0} run: | conda activate ${{ env.CONDAENV }} anaconda -t=${{ secrets.ANACONDA_TOKEN }} upload ${{ env.CONDAROOT }}/conda-bld/${{ matrix.platform.short }}/ada-py-${{ env.PKG_VERSION }}-${{ matrix.pyver.name }}_0.tar.bz2 --user krande --skip-existing pypi: - if: github.event_name == 'push' && github.ref == 'refs/heads/main && ${{ github.event.workflow_run.conclusion == 'success' }} + if: github.event_name == 'push' && github.ref == 'refs/heads/main && github.event.workflow_run.conclusion == 'success' needs: test name: Publish to PYPI runs-on: ubuntu-latest diff --git a/images/tests/build_verification_report.py b/images/tests/build_verification_report.py new file mode 100644 index 000000000..972a447cb --- /dev/null +++ b/images/tests/build_verification_report.py @@ -0,0 +1,106 @@ +import logging +from typing import Dict, Union + +import pandas as pd +from common import append_df, eig_data_to_df, make_eig_fem +from paradoc import OneDoc +from paradoc.common import TableFormat + +from ada import Beam, Material +from ada.materials.metals import CarbonSteel + + +def shorten_name(name, fem_format, geom_repr) -> str: + short_name = name.replace("cantilever_EIG_", "") + short_name_map = dict(calculix="ccx", code_aster="ca", abaqus="aba", sesam="ses") + geom_repr_map = dict(solid="so", line="li", shell="sh") + short_name = short_name.replace(fem_format, short_name_map[fem_format]) + short_name = short_name.replace(geom_repr, geom_repr_map[geom_repr]) + + return short_name + + +def run_and_postprocess(bm, soft, geo, elo, df_write_map, results, overwrite, execute, eig_modes): + res = make_eig_fem(bm, soft, geo, elo, overwrite=overwrite, execute=execute, eigen_modes=eig_modes) + if res is None or res.eigen_mode_data is None: + logging.error("No result file is located") + return None + else: + short_name = shorten_name(res.name, soft, geo) + df = eig_data_to_df(res.eigen_mode_data, ["Mode", short_name]) + df_current = df_write_map.get(geo) + if df_current is not None: + df = df[short_name] + df_write_map[geo] = append_df(df, df_current) + results.append(res) + + +def simulate(bm, el_order, geom_repr, analysis_software, eig_modes, overwrite, execute): + results = [] + merged_line_df = None + merged_shell_df = None + merged_solid_df = None + + df_write_map: Dict[str, Union[pd.DataFrame, None]] = dict( + line=merged_line_df, shell=merged_shell_df, solid=merged_solid_df + ) + + for elo in el_order: + for geo in geom_repr: + for soft in analysis_software: + try: + run_and_postprocess(bm, soft, geo, elo, df_write_map, results, overwrite, execute, eig_modes) + except IOError as e: + logging.error(e) + return results, df_write_map + + +def main(overwrite, execute): + analysis_software = ["calculix", "code_aster"] + el_order = [1, 2] + geom_repr = ["line", "shell", "solid"] + eig_modes = 11 + + bm = Beam( + "MyBeam", + (0, 0.5, 0.5), + (3, 0.5, 0.5), + "IPE400", + Material("S420", CarbonSteel("S420")), + ) + + one = OneDoc("report") + + one.variables = dict( + geom_specifics=str(bm), + ca_version=14.2, + ccx_version=2.16, + aba_version=2021, + ses_version=10, + num_modes=eig_modes, + ) + + table_format = TableFormat(font_size=8, float_fmt=".3f") + + results, df_write_map = simulate(bm, el_order, geom_repr, analysis_software, eig_modes, overwrite, execute) + + for geo in geom_repr: + one.add_table( + f"eig_compare_{geo}", + df_write_map[geo], + f"Comparison of all Eigenvalue analysis using {geo} elements", + tbl_format=table_format, + ) + + for res in results: + one.add_table( + res.name, + eig_data_to_df(res.eigen_mode_data, ["Mode", "Eigenvalue (real)"]), + res.name, + ) + + one.compile("ADA-FEA-verification") + + +if __name__ == "__main__": + main(overwrite=False, execute=False) diff --git a/images/tests/common.py b/images/tests/common.py new file mode 100644 index 000000000..1bae16126 --- /dev/null +++ b/images/tests/common.py @@ -0,0 +1,168 @@ +import logging +import os +import pathlib + +import pandas as pd + +from ada import Assembly, Beam, Material, Part +from ada.config import Settings +from ada.fem import Bc, FemSet, Load, StepEigen, StepImplicit +from ada.fem.concepts.eigenvalue import EigenDataSummary +from ada.fem.exceptions import FEASolverNotInstalled, IncompatibleElements +from ada.fem.formats.utils import default_fem_res_path +from ada.fem.meshing.concepts import GmshOptions, GmshSession +from ada.fem.results import Results +from ada.fem.utils import get_beam_end_nodes, get_eldata +from ada.materials.metals import CarbonSteel + + +def static_cantilever(): + beam = Beam( + "MyBeam", + (0, 0.5, 0.5), + (3, 0.5, 0.5), + "IPE400", + Material("S420", CarbonSteel("S420")), + ) + + p = Part("MyPart") + a = Assembly("MyAssembly") / [p / beam] + + p.fem = beam.to_fem_obj(0.1, "shell", options=GmshOptions(Mesh_ElementOrder=2)) + + fix_set = p.fem.add_set(FemSet("bc_nodes", get_beam_end_nodes(beam), FemSet.TYPES.NSET)) + + load_set = p.fem.add_set(FemSet("load_node", get_beam_end_nodes(beam, 2), FemSet.TYPES.NSET)) + a.fem.add_bc(Bc("Fixed", fix_set, [1, 2, 3, 4, 5, 6])) + step = a.fem.add_step(StepImplicit("StaticStep")) + step.add_load(Load("MyLoad", Load.TYPES.FORCE, -15e3, load_set, 3)) + return a + + +def make_eig_fem( + beam: Beam, + fem_format, + geom_repr, + elem_order=1, + incomplete_2nd_order=True, + overwrite=True, + execute=True, + eigen_modes=10, +): + name = f"cantilever_EIG_{fem_format}_{geom_repr}_o{elem_order}" + scratch_dir = Settings.scratch_dir / "eigen_fem" + fem_res_files = default_fem_res_path(name, scratch_dir=scratch_dir) + + p = Part("MyPart") + a = Assembly("MyAssembly") / [p / beam] + os.makedirs("temp", exist_ok=True) + if fem_res_files[fem_format].exists() is False: + execute = True + overwrite = True + + mesh_size = 0.05 if geom_repr != "line" else 0.3 + + if overwrite is True: + gmsh_opt = GmshOptions(Mesh_ElementOrder=elem_order, Mesh_MeshSizeMin=mesh_size) + gmsh_opt.Mesh_SecondOrderIncomplete = 1 if incomplete_2nd_order is True else 0 + p.fem = beam.to_fem_obj(mesh_size, geom_repr, options=gmsh_opt) + fs = p.fem.add_set(FemSet("bc_nodes", beam.bbox.sides.back(return_fem_nodes=True))) + a.fem.add_bc(Bc("Fixed", fs, [1, 2, 3, 4, 5, 6])) + + a.fem.add_step(StepEigen("EigenStep", num_eigen_modes=eigen_modes)) + + try: + res = a.to_fem( + name, + fem_format, + overwrite=overwrite, + execute=execute, + scratch_dir=scratch_dir, + ) + except IncompatibleElements as e: + logging.error(e) + return None + except FEASolverNotInstalled as e: + logging.error(e) + return None + except BaseException as e: + raise Exception(e) + if res.output is not None: + os.makedirs("temp/logs", exist_ok=True) + with open(f"temp/logs/{name}.log", "w") as f: + f.write(res.output.stdout) + + if res.eigen_mode_data is not None: + for eig in res.eigen_mode_data.modes: + print(eig) + else: + logging.error("Result file not created") + + assert pathlib.Path(res.results_file_path).exists() + return res + + +def make_2nd_order_complete_elements(): + overwrite = False + execute = False + fem_format = "code_aster" + elem_order = 2 + beam = Beam( + "MyBeam", + (0, 0.5, 0.5), + (3, 0.5, 0.5), + "IPE400", + Material("S420", CarbonSteel("S420")), + ) + geom_repr = "solid" + gmsh_opt = GmshOptions(Mesh_ElementOrder=elem_order, Mesh_SecondOrderIncomplete=0) + + a = Assembly("MyAssembly") / [Part("MyPart") / beam] + + with GmshSession(silent=False, options=gmsh_opt) as gs: + gs.add_obj(beam, geom_repr=geom_repr) + gs.options.Mesh_Algorithm = 6 + gs.options.Mesh_Algorithm3D = 10 + # gs.open_gui() + gs.mesh(0.05) if geom_repr != "line" else gs.mesh(1.0) + a.get_part("MyPart").fem = gs.get_fem() + print(get_eldata(a)) + fix_set = a.get_part("MyPart").fem.add_set(FemSet("bc_nodes", get_beam_end_nodes(beam), FemSet.TYPES.NSET)) + a.fem.add_bc(Bc("Fixed", fix_set, [1, 2, 3, 4, 5, 6])) + a.fem.add_step(StepEigen("Eigen", num_eigen_modes=10)) + + name = f"cantilever_EIG_{fem_format}_{geom_repr}_o{elem_order}" + res = a.to_fem(name, fem_format, overwrite=overwrite, execute=execute) + + if res.eigen_mode_data is not None: + for eig in res.eigen_mode_data.modes: + print(eig) + + assert pathlib.Path(res.results_file_path).exists() + return res + + +def append_df(new_df, old_df): + if old_df is None: + updated_df = new_df + else: + updated_df = pd.concat([old_df, new_df], axis=1) + return updated_df + + +def eig_data_to_df(eig_data: EigenDataSummary, columns): + return pd.DataFrame([(e.no, e.f_hz) for e in eig_data.modes], columns=columns) + + +def eig_result_to_table(res: Results): + eig_data = res.eigen_mode_data + df = pd.DataFrame([(e.no, e.f_hz) for e in eig_data.modes], columns=["Mode", "Eigenvalue (real)"]) + return df.to_markdown(index=False, tablefmt="grid") + + +if __name__ == "__main__": + a = static_cantilever() + scratch = Settings.scratch_dir / "ada-testing" + opts = dict(execute=True, overwrite=True, scratch_dir=scratch) + a.to_fem("MyCantileverLoadTest_sesam", "sesam", **opts) + a.to_fem("MyCantileverLoadTest_abaqus", "abaqus", **opts) diff --git a/images/tests/report/00-main/00-intro.md b/images/tests/report/00-main/00-intro.md new file mode 100644 index 000000000..540552506 --- /dev/null +++ b/images/tests/report/00-main/00-intro.md @@ -0,0 +1,47 @@ +# Open Source FEA using adapy + +This report describes the main verification work of the simulation results produced by the +Open Source Finite Element (FE) solvers [Code Aster](https://www.code-aster.org/spip.php?rubrique2) +and [Calculix](http://www.dhondt.de/). + +The motivation behind producing this report is to increase confidence in the conversion of FEM models in _adapy_ +and also the results from the various FEA solvers. + +## Introduction + +The results from _Code Aster_ and _Calculix_ and tools will be compared with proprietary FEA solvers such as; + + +* [Abaqus](https://www.3ds.com/products-services/simulia/products/abaqus/) +* [Sestra (part of DNV's Sesam suite)](https://www.dnv.com/services/linear-structural-analysis-sestra-2276) + + +The simulations are pre- and post processed using [adapy](https://github.com/Krande/adapy) and this report is +automatically generated using [paradoc](https://github.com/Krande/paradoc) as +part of a ci/cd pipeline within the _adapy_ repositories. + +For each new addition of code affecting the handling of +FEM in _adapy_ a new series of analysis is performed whereas the results are gathered in this auto-generated document. + + +## FEA Solvers + +* Calculix v{{__ccx_version__}} +* Code Aster v{{__ca_version__}} +* Abaqus v{{__aba_version__}} +* Sestra v{{__ses_version__}} + + +## Python packages +The following python packages were instrumental in the creation of this document and the FEA results herein. + +### Adapy + +The intention behind _adapy_ is to make it easier to work with finite element models +and BIM models. + +### Paradoc + +_paradoc_ was created to simplify the generation of reports by creating the structure of the document and text +in markdown with a string substitution scheme that lets you easily pass in tables, functions and finally be +able to produce production ready documents in Microsoft Word. diff --git a/images/tests/report/00-main/01-eigenvalue-summary.md b/images/tests/report/00-main/01-eigenvalue-summary.md new file mode 100644 index 000000000..fc295d9f3 --- /dev/null +++ b/images/tests/report/00-main/01-eigenvalue-summary.md @@ -0,0 +1,45 @@ +# Summary + +Below is a summary of the performed analysis + +## Eigenvalue Analysis + +A simple cantilevered beam is subjected to eigenvalue analysis with a total of +{{__num_modes__}} eigenmodes requested. + + +### Model Description + +Model object: {{__geom_specifics__}} + +### Summary + +This section presents the resulting comparison between the calculated results from the +eigenvalue analysis in the different FEA tools. + + +{{__eig_compare_solid__}} + + +{{__eig_compare_shell__}} + + +{{__eig_compare_line__}} + + + +**Solid Elements** + +As shown in the tables above, the differences between the various FEA tools are small for +solid elements both for 1st and 2nd order formulations. + +**Shell Elements** + +For 1st order shell elements it is observed an increasing difference in values based on +the order of eigenmode. +Results using 2nd order shell elements is observed to be closer for all modes. + +**Line Elements** + +1st order line elements in Code Aster and Abaqus differ slightly from mode #4 and higher. +Calculix does not support eigenvalue analysis using generalized U1 beam elements. diff --git a/images/tests/report/00-main/02-nonlinear-summary.md b/images/tests/report/00-main/02-nonlinear-summary.md new file mode 100644 index 000000000..27e802d49 --- /dev/null +++ b/images/tests/report/00-main/02-nonlinear-summary.md @@ -0,0 +1,8 @@ +## Nonlinear Analysis + +A simple cantilever beam with a series of cutouts is subjugated to a large vertical acceleration field. The level +of plastic strains are monitored for comparison across different FE tools. + +### Model Description + +### Summary \ No newline at end of file diff --git a/images/tests/report/01-app/00-results-detailed.md b/images/tests/report/01-app/00-results-detailed.md new file mode 100644 index 000000000..4c5ea1197 --- /dev/null +++ b/images/tests/report/01-app/00-results-detailed.md @@ -0,0 +1,38 @@ +# Eigenvalue analysis detailed results + +## Calculix +Using Calculix v{{__ccx_version__}} the following results were obtained. + + + +{{__cantilever_EIG_calculix_shell_o1__}} + + + +{{__cantilever_EIG_calculix_shell_o2__}} + + + +{{__cantilever_EIG_calculix_solid_o1__}} + + + +{{__cantilever_EIG_calculix_solid_o2__}} + + + +## Code Aster +Using Code Aster v{{__ca_version__}} the following results were obtained. + + + +{{__cantilever_EIG_code_aster_shell_o1__}} + + +{{__cantilever_EIG_code_aster_shell_o2__}} + + +{{__cantilever_EIG_code_aster_solid_o1__}} + + +{{__cantilever_EIG_code_aster_solid_o2__}} diff --git a/images/tests/report/metadata.yaml b/images/tests/report/metadata.yaml new file mode 100644 index 000000000..b8066d17b --- /dev/null +++ b/images/tests/report/metadata.yaml @@ -0,0 +1,5 @@ +linkReferences: false +numbersections: false +nameInLink: true +figPrefix: "Figure" +tblPrefix: "Table" \ No newline at end of file diff --git a/images/tests/test_fem_eig_cantilever.py b/images/tests/test_fem_eig_cantilever.py index b588b26ab..da32609b5 100644 --- a/images/tests/test_fem_eig_cantilever.py +++ b/images/tests/test_fem_eig_cantilever.py @@ -3,13 +3,13 @@ import pytest -from ada import Assembly, Beam, Material, Part -from ada.fem import Bc, FemSet, StepEigen +import ada from ada.fem.exceptions.element_support import IncompatibleElements -from ada.fem.meshing.concepts import GmshOptions, GmshSession -from ada.fem.utils import get_beam_end_nodes +from ada.fem.meshing.concepts import GmshOptions from ada.materials.metals import CarbonSteel +test_dir = ada.config.Settings.scratch_dir / "eigen_fem" + @pytest.mark.parametrize("fem_format", ["code_aster", "calculix"]) @pytest.mark.parametrize("geom_repr", ["line", "shell", "solid"]) @@ -17,20 +17,16 @@ def test_fem_eig(fem_format, geom_repr, elem_order): name = f"cantilever_EIG_{fem_format}_{geom_repr}_o{elem_order}" - beam = Beam("MyBeam", (0, 0.5, 0.5), (3, 0.5, 0.5), "IPE400", Material("S420", CarbonSteel("S420"))) - a = Assembly("MyAssembly") / [Part("MyPart") / beam] - - with GmshSession(silent=True, options=GmshOptions(Mesh_ElementOrder=elem_order)) as gs: - gs.add_obj(beam, geom_repr=geom_repr) - gs.mesh(0.05) - a.get_part("MyPart").fem = gs.get_fem() - - fix_set = a.get_part("MyPart").fem.add_set(FemSet("bc_nodes", get_beam_end_nodes(beam), FemSet.TYPES.NSET)) - a.fem.add_bc(Bc("Fixed", fix_set, [1, 2, 3, 4, 5, 6])) - a.fem.add_step(StepEigen("Eigen", num_eigen_modes=11)) + beam = ada.Beam("MyBeam", (0, 0.5, 0.5), (3, 0.5, 0.5), "IPE400", ada.Material("S420", CarbonSteel("S420"))) + p = ada.Part("MyPart") + a = ada.Assembly("MyAssembly") / [p / beam] + p.fem = beam.to_fem_obj(0.05, geom_repr, options=GmshOptions(Mesh_ElementOrder=elem_order)) + fix_set = p.fem.add_set(ada.fem.FemSet("bc_nodes", beam.bbox.sides.back(return_fem_nodes=True, fem=p.fem))) + a.fem.add_bc(ada.fem.Bc("Fixed", fix_set, [1, 2, 3, 4, 5, 6])) + a.fem.add_step(ada.fem.StepEigen("Eigen", num_eigen_modes=11)) try: - res = a.to_fem(name, fem_format, overwrite=True, execute=True) + res = a.to_fem(name, fem_format, overwrite=True, execute=True, scratch_dir=test_dir) except IncompatibleElements as e: if fem_format == "calculix" and geom_repr == "line": logging.error(e) @@ -42,7 +38,3 @@ def test_fem_eig(fem_format, geom_repr, elem_order): if pathlib.Path(res.results_file_path).exists() is False: raise FileNotFoundError(f'FEM analysis was not successful. Result file "{res.results_file_path}" not found.') - - -if __name__ == "__main__": - test_fem_eig("code_aster", "solid", 2)