Skip to content

Commit

Permalink
Add verification report for adapy in ci/cd pipeline
Browse files Browse the repository at this point in the history
  • Loading branch information
Krande committed Oct 30, 2021
1 parent c350840 commit e135304
Show file tree
Hide file tree
Showing 10 changed files with 443 additions and 23 deletions.
13 changes: 12 additions & 1 deletion .github/workflows/ci-FEM.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}"
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
106 changes: 106 additions & 0 deletions images/tests/build_verification_report.py
Original file line number Diff line number Diff line change
@@ -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)
168 changes: 168 additions & 0 deletions images/tests/common.py
Original file line number Diff line number Diff line change
@@ -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)
47 changes: 47 additions & 0 deletions images/tests/report/00-main/00-intro.md
Original file line number Diff line number Diff line change
@@ -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.
Loading

0 comments on commit e135304

Please sign in to comment.