diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..a941c36 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,35 @@ +name: Deploy Documentation + +on: + push: + branches: + - main # Replace with your default branch if it's not 'main' + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + + steps: + - name: Check Out Repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install sphinx + + - name: Build Documentation + run: | + cd docs + make html + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs/build/html \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c85c671 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# directories +*/*.egg-info/ +.ipynb_checkpoints/ +Lectures/src/.vscode/ +.vscode/ + + + +# latex +*.vrb +*.toc +*.snm +*.out +*.nav +*.gz +*.log +*.aux +*.glo +*.ist +*.acn +*.synctex(busy) +*.pyc +Lectures/*/*.pdf +/.pytest_cache/ +/docs/build/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..79634b2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "python.testing.pytestArgs": [ + "src" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "esbonio.sphinx.confDir": "" +} \ No newline at end of file diff --git a/README.md b/README.md index 4e71881..bae2fa3 100644 --- a/README.md +++ b/README.md @@ -138,4 +138,5 @@ In example 2, all members properties are **E = 200 GPa** and **A = 0.0015 m^2^** ## Author -- Eduardo Pereira \ No newline at end of file +- Eduardo Pereira (original MATLAB/GNU Octave version) +- Nikolaos Papadakis (Python implementation) \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..5eb2ae6 --- /dev/null +++ b/TODO.md @@ -0,0 +1,21 @@ + +## TODO +- CLEANUP: Streamline code (use np.arrays for easier slicing etc) +- CLEANUP: plotter functions could use a lot of cleanup (i.e. move the Mesh, Displacement +and Solution objects into the _plot_functions) +- DOC: add documentation for all methods +- DOC: add documentation for classes +- DOC: add quickstart guide +- CLEANUP: use the roots __init__.py imports to simplify the import statements + +## History +- 20240128|ORG: Upload package to Pypi +- 20240128|DOC: add documentation with example cases + +- 20240128| ORG: USe single source of truth for versioning +- 20240128| FEATURE: complete conversion of PlotDeformation +- 20240128| FEATURE: complete conversion of PlotStress +- 20240128| FEATURE: Provide alternative ways to load data (e.g. json, directly with np.arrays) +- 20240127| ORG: Create a master class that performs seemleasly all the code (e.g. TrussProblem or similar). +- 20240127| ORG: Create a python package + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..efcee55 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,4 @@ +There are many problems that can be used for validation in + +# https://mathalino.com/reviewer/engineering-mechanics/problem-004-mj-method-joints + diff --git a/docs/source/background.rst b/docs/source/background.rst new file mode 100644 index 0000000..a5a01f4 --- /dev/null +++ b/docs/source/background.rst @@ -0,0 +1,6 @@ +Truss Analysis Bakcground +========================= + +This section is work in progress. + +It will be used to explain the truss solving process. \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..7ad1d7f --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,64 @@ + +import os +import sys +sys.path.insert(0, os.path.abspath('../src')) + +def get_version(rel_path): + """Get the version string from a file. + Assuming the version line is in the form: __version__ = '0.1.0' + strips out the version and remove leading and trailing whitespace and quotes + Args: + rel_path (str): The relative path to the file. +Raises: + RuntimeError: If the version string is not found. + + Returns: + str: The version string. + """ + + with open(rel_path, 'r', encoding='utf-8') as fp: + for line in fp: + if line.startswith('__version__'): + delim = '"' if '"' in line else "'" + return line.split(delim)[1] + raise RuntimeError("Unable to find version string.") + +PATH_TO_INIT_PY = '../../src/npp_2d_truss_analysis/__init__.py' + +__version__ = get_version(PATH_TO_INIT_PY) + +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'NPP 2d Truss Analysis' +copyright = '2024, Nikolaos Papadakis' +author = 'Nikolaos Papadakis' +release = __version__ + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [] + +templates_path = ['_templates'] +exclude_patterns = [] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] + + + +rst_epilog = """ +.. |ProjectVersion| replace:: v{versionnum} +""".format(versionnum = release +) \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..e3f072a --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,25 @@ +.. NPP 2d Truss Analysis documentation master file, created by + sphinx-quickstart on Mon Jan 29 09:58:27 2024. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to NPP 2d Truss Analysis's |ProjectVersion| documentation! +================================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + introduction + background + installation + tutorials/index + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/installation.rst b/docs/source/installation.rst new file mode 100644 index 0000000..0120cd6 --- /dev/null +++ b/docs/source/installation.rst @@ -0,0 +1,42 @@ + +Installation/Setup +================== + +Requirements +------------ + +The package depends on the following "standard" libraries: + +- matplotlib +- scipy +- numpy + + +Conda Installation +------------------ + +The following describes a minimum environment setup using Conda. + +(Optional) It is preferable to create a new environment for the packages: + +.. code-block:: bash + + conda create -n truss_analysis python=3 + +The installation requires: + +- matplotlib +- scipy +- numpy + +To install these packages, use the command: + +.. code-block:: bash + + conda install matplotlib scipy numpy + +Then ton install the package, use the command: + +.. code-block:: bash + + pip install npp_2d_truss_analysis \ No newline at end of file diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst new file mode 100644 index 0000000..6131e59 --- /dev/null +++ b/docs/source/introduction.rst @@ -0,0 +1,20 @@ +Introduction +------------ + +**npp_2d_truss_analysis** is an open-source software tool designed for the solving of trusses with features like: + +- inclined roller supports +- offset pin supports +- prescribed displacements +- plotting of Truss problem (with geometry, loads and supports) +- deformation plotting +- forces and reaction plotting + +Developers +---------- + +The python version of the truss analysis project has been developed by: + +- N. Papadakis: In charge of software design and package maintenance. + +the work is based on Eduaardo's Pereira's repoistory for matlab called [TrussAnalysis2d-MATLAB](https://github.com/edurodriper/TrussAnalysis2D-MATLAB). diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst new file mode 100644 index 0000000..96ff608 --- /dev/null +++ b/docs/source/tutorials/index.rst @@ -0,0 +1,18 @@ +Tutorials +========= + +.. toctree:: + :maxdepth: 2 + :caption: Tutorials: + + quickstart + tutorial_original_input + tutorial_json_input + tutorial_single_file_json + + +Additional things to consider: + +- how to update input for forces + +- plotting only truss without the solution \ No newline at end of file diff --git a/docs/source/tutorials/quickstart.rst b/docs/source/tutorials/quickstart.rst new file mode 100644 index 0000000..ea8f12d --- /dev/null +++ b/docs/source/tutorials/quickstart.rst @@ -0,0 +1,233 @@ +quickstart +========== + +This tutorial demonstrates a quick way to get started. + +We will use a JSON configuration to define the truss structure and then perform an analysis to determine the reactions and rod forces and plot the results. + +Installation +------------ + +Use + +.. code-block:: bash + + pip install npp_2d_truss_analysis + + +Import Necessary Libraries/Setup +-------------------------------- + +To start, we need to import necessary modules and define the project directory and file name prefix. + +.. code-block:: python + + import pathlib + + from npp_2d_truss_analysis import Info, TrussAnalysisProject + + pp_project_dir = pathlib.Path('./') + FNAME_PREFIX = 'test' + + info = Info(project_directory=str(pp_project_dir.absolute()), file_name=FNAME_PREFIX) + + +Defining the Truss Structure +---------------------------- +The truss structure is defined using a JSON string. This includes the definition of nodes, elements, materials, displacements, and forces. + + +.. code-block:: python + + JSON_TEXT="""{ + "mesh":{ + "nodes": [ + {"id": 1, "coordinates": [0, 0]}, + {"id": 2, "coordinates": [0, 2]}, + {"id": 3, "coordinates": [0, 4]}, + {"id": 4, "coordinates": [4, 4]}, + {"id": 5, "coordinates": [4, 2]} + ], + "elements": [ + {"id": 1, "connectivity": [4, 3], "materialId": 1}, + {"id": 2, "connectivity": [3, 2], "materialId": 1}, + {"id": 3, "connectivity": [2, 4], "materialId": 1}, + {"id": 4, "connectivity": [4, 5], "materialId": 1}, + {"id": 5, "connectivity": [5, 2], "materialId": 1}, + {"id": 6, "connectivity": [2, 1], "materialId": 1}, + {"id": 7, "connectivity": [1, 5], "materialId": 1} + ], + "materials": [ + {"id": 1, "youngModulus": 200000000000.0, "area": 1.0} + ] + }, + "displacements":{ + "pin": [ + {"id": 1, "node":4, "angle": 0,"dx": 0, "dy":0} + ], + "rollers": [ + {"id": 1, "node": 1, "direction": 1, "angle": -63.4349, "dx":0} + ] + }, + "forces": [ + {"id": 1, "node":5, "direction": -180,"x": 40000, "y":0}, + {"id": 2, "node":3, "direction": 200,"x": 20000, "y":0} + ] + }""" + +Creating the Truss Problem +-------------------------- + +We instantiate the truss problem from the JSON text. + +.. code-block:: python + + truss_problem = TrussAnalysisProject.from_json(json_text=JSON_TEXT, + + +Updating Forces (optional) +-------------------------- + +Before solving the problem, we update the forces as needed. + +.. code-block:: python + + truss_problem._forces.update_force_by_id(force_id=1, angle=180+20) + + +Listing Forces (Optional) +------------------------- + +Optionally, list the forces to verify the updates. + +.. code-block:: python + + truss_problem._forces.list_forces() + +Solving the Problem +------------------- + +Now, we write the input data and solve the truss problem. + +.. code-block:: python + + truss_problem.write_input_data() + # truss_problem.update_matrices() # optional because solve automatically does that + truss_problem.solve() + + +Reporting the Solution +---------------------- + +Finally, we print the solution, including the reactions and rod forces. + +.. code-block:: python + + print("-------------solution ----------------") + truss_problem.report_reactions(fmt='>12.1f') + truss_problem.report_rod_forces(fmt='>12.1f') + + +Plotting the truss +------------------ + +It is also possible to plot the truss using the following code: + + +.. code-block:: python + + truss_problem.plot_truss(save=True, show=True) + +using the flags save and show to save the plot to a file and/or show the plot on the screen. + + + +Plotting truss deformation and stresses +--------------------------------------- + +After solving the problem, we can plot the deformed state and the stresses. + + +.. code-block:: python + + truss_problem.plot_deformation(save=True, show=True) + truss_problem.plot_stresses(save=True, show=True) + +using the flags save and show to save the plot to a file and/or show the plot on the screen. + +The color of the rods in the stress plot indicates the stress level and whether it is in tension or compression. The color of the rods in the deformation plot indicates the displacement level and whether it is in tension or compression. +More specifically, the color of the rods in the stress plot will be : + +- blue: if the rod is in tension + +- red: if the rod is in compression + + +Complete Code of the Tutorial +----------------------------- + + +.. code-block:: python + + import pathlib + + from npp_2d_truss_analysis import Info, TrussAnalysisProject + + pp_project_dir = pathlib.Path('./') + FNAME_PREFIX = 'test' + + info = Info(project_directory=str(pp_project_dir.absolute()), file_name=FNAME_PREFIX) + + JSON_TEXT="""{ + "mesh":{ + "nodes": [ + {"id": 1, "coordinates": [0, 0]}, + {"id": 2, "coordinates": [0, 2]}, + {"id": 3, "coordinates": [0, 4]}, + {"id": 4, "coordinates": [4, 4]}, + {"id": 5, "coordinates": [4, 2]} + ], + "elements": [ + {"id": 1, "connectivity": [4, 3], "materialId": 1}, + {"id": 2, "connectivity": [3, 2], "materialId": 1}, + {"id": 3, "connectivity": [2, 4], "materialId": 1}, + {"id": 4, "connectivity": [4, 5], "materialId": 1}, + {"id": 5, "connectivity": [5, 2], "materialId": 1}, + {"id": 6, "connectivity": [2, 1], "materialId": 1}, + {"id": 7, "connectivity": [1, 5], "materialId": 1} + ], + "materials": [ + {"id": 1, "youngModulus": 200000000000.0, "area": 1.0} + ] + }, + "displacements":{ + "pin": [ + {"id": 1, "node":4, "angle": 0,"dx": 0, "dy":0} + ], + "rollers": [ + {"id": 1, "node": 1, "direction": 1, "angle": -63.4349, "dx":0} + ] + }, + "forces": [ + {"id": 1, "node":5, "direction": -180,"x": 40000, "y":0}, + {"id": 2, "node":3, "direction": 200,"x": 20000, "y":0} + ] + }""" + + + truss_problem = TrussAnalysisProject.from_json(json_text=JSON_TEXT, info=info) + + truss_problem._forces.update_force_by_id(force_id=1, angle=180+20) + truss_problem._forces.list_forces() + truss_problem.write_input_data() + truss_problem.plot_truss(save=True, show=True) + + # solution + truss_problem.solve() + print("-------------solution ----------------") + truss_problem.report_reactions(fmt='>12.1f') + truss_problem.report_rod_forces(fmt='>12.1f') + # plotting results + truss_problem.plot_deformation(save=True, show=True) + truss_problem.plot_stresses(save=True, show=True) + diff --git a/docs/source/tutorials/tutorial_json_input.rst b/docs/source/tutorials/tutorial_json_input.rst new file mode 100644 index 0000000..ce6504a --- /dev/null +++ b/docs/source/tutorials/tutorial_json_input.rst @@ -0,0 +1,28 @@ +Tutorial json input +==================== + +This tutorial demonstrates the way of using json to inputing the problem definition for Truss Analysis project. + + + + +Import Necessary Libraries +-------------------------- + +First, import the required modules: + +.. code-block:: python + + import pathlib + import numpy as np + import tkinter as tk + from tkinter import filedialog + + from npp_2d_truss_analysis.truss_input import Info, FileData,Mesh,Displacements,Forces, write_input_data + from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis + from npp_2d_truss_analysis.truss_solution import Solution, write_results + from npp_2d_truss_analysis.truss_plotter import TrussPlotter + from npp_2d_truss_analysis.truss_project import TrussAnalysisProject + + +**WORK IN PROGRESS** \ No newline at end of file diff --git a/docs/source/tutorials/tutorial_original_input.rst b/docs/source/tutorials/tutorial_original_input.rst new file mode 100644 index 0000000..ffb5f03 --- /dev/null +++ b/docs/source/tutorials/tutorial_original_input.rst @@ -0,0 +1,22 @@ +Tutorial original input +======================= + +This tutorial demonstrates the initial way of importing the data for Truss Analysis project. + + + + +Import Necessary Libraries +-------------------------- + +First, import the required modules: + +.. code-block:: python + + import numpy as np + import matplotlib.pyplot as plt + import npp_2d_truss_analysis + + +**WORK IN PROGRESS** + diff --git a/docs/source/tutorials/tutorial_single_file_json.rst b/docs/source/tutorials/tutorial_single_file_json.rst new file mode 100644 index 0000000..f4ac889 --- /dev/null +++ b/docs/source/tutorials/tutorial_single_file_json.rst @@ -0,0 +1,223 @@ +Tutorial for single file json input +===================================== + +This tutorial demonstrates the way of using json file to input the problem definition for Truss Analysis project. + +We will use a JSON configuration to define the truss structure and then perform an analysis to determine the reactions and rod forces. + +Import Necessary Libraries/Setup +-------------------------------- + +To start, we need to import necessary modules and define the project directory and file name prefix. + +.. code-block:: python + + import pathlib + + from npp_2d_truss_analysis import Info, TrussAnalysisProject + + pp_project_dir = pathlib.Path('./') + FNAME_PREFIX = 'test' + + info = Info(project_directory=str(pp_project_dir.absolute()), file_name=FNAME_PREFIX) + + +Defining the Truss Structure +---------------------------- +The truss structure is defined using a JSON string. This includes the definition of nodes, elements, materials, displacements, and forces. + + +.. code-block:: python + + JSON_TEXT="""{ + "mesh":{ + "nodes": [ + {"id": 1, "coordinates": [0, 0]}, + {"id": 2, "coordinates": [0, 2]}, + {"id": 3, "coordinates": [0, 4]}, + {"id": 4, "coordinates": [4, 4]}, + {"id": 5, "coordinates": [4, 2]} + ], + "elements": [ + {"id": 1, "connectivity": [4, 3], "materialId": 1}, + {"id": 2, "connectivity": [3, 2], "materialId": 1}, + {"id": 3, "connectivity": [2, 4], "materialId": 1}, + {"id": 4, "connectivity": [4, 5], "materialId": 1}, + {"id": 5, "connectivity": [5, 2], "materialId": 1}, + {"id": 6, "connectivity": [2, 1], "materialId": 1}, + {"id": 7, "connectivity": [1, 5], "materialId": 1} + ], + "materials": [ + {"id": 1, "youngModulus": 200000000000.0, "area": 1.0} + ] + }, + "displacements":{ + "pin": [ + {"id": 1, "node":4, "angle": 0,"dx": 0, "dy":0} + ], + "rollers": [ + {"id": 1, "node": 1, "direction": 1, "angle": -63.4349, "dx":0} + ] + }, + "forces": [ + {"id": 1, "node":5, "direction": -180,"x": 40000, "y":0}, + {"id": 2, "node":3, "direction": 200,"x": 20000, "y":0} + ] + }""" + +Creating the Truss Problem +-------------------------- + +We instantiate the truss problem from the JSON text. + +.. code-block:: python + + truss_problem = TrussAnalysisProject.from_json(json_text=JSON_TEXT, + + +Updating Forces (optional) +-------------------------- + +Before solving the problem, we update the forces as needed. + +.. code-block:: python + + truss_problem._forces.update_force_by_id(force_id=1, angle=180+20) + + +Listing Forces (Optional) +------------------------- + +Optionally, list the forces to verify the updates. + +.. code-block:: python + + truss_problem._forces.list_forces() + +Solving the Problem +------------------- + +Now, we write the input data and solve the truss problem. + +.. code-block:: python + + truss_problem.write_input_data() + # truss_problem.update_matrices() # optional because solve automatically does that + truss_problem.solve() + + +Reporting the Solution +---------------------- + +Finally, we print the solution, including the reactions and rod forces. + +.. code-block:: python + + print("-------------solution ----------------") + truss_problem.report_reactions(fmt='>12.1f') + truss_problem.report_rod_forces(fmt='>12.1f') + + +Plotting the truss +------------------ + +It is also possible to plot the truss using the following code: + + +.. code-block:: python + + truss_problem.plot_truss(save=True, show=True) + +using the flags save and show to save the plot to a file and/or show the plot on the screen. + + + +Plotting truss deformation and stresses +--------------------------------------- + +After solving the problem, we can plot the deformed state and the stresses. + + +.. code-block:: python + + truss_problem.plot_deformation(save=True, show=True) + truss_problem.plot_stresses(save=True, show=True) + +using the flags save and show to save the plot to a file and/or show the plot on the screen. + +The color of the rods in the stress plot indicates the stress level and whether it is in tension or compression. The color of the rods in the deformation plot indicates the displacement level and whether it is in tension or compression. +More specifically, the color of the rods in the stress plot will be : + +- blue: if the rod is in tension + +- red: if the rod is in compression + + +Complete Code of the Tutorial +----------------------------- + + +.. code-block:: python + + import pathlib + + from npp_2d_truss_analysis import Info, TrussAnalysisProject + + pp_project_dir = pathlib.Path('./') + FNAME_PREFIX = 'test' + + info = Info(project_directory=str(pp_project_dir.absolute()), file_name=FNAME_PREFIX) + + JSON_TEXT="""{ + "mesh":{ + "nodes": [ + {"id": 1, "coordinates": [0, 0]}, + {"id": 2, "coordinates": [0, 2]}, + {"id": 3, "coordinates": [0, 4]}, + {"id": 4, "coordinates": [4, 4]}, + {"id": 5, "coordinates": [4, 2]} + ], + "elements": [ + {"id": 1, "connectivity": [4, 3], "materialId": 1}, + {"id": 2, "connectivity": [3, 2], "materialId": 1}, + {"id": 3, "connectivity": [2, 4], "materialId": 1}, + {"id": 4, "connectivity": [4, 5], "materialId": 1}, + {"id": 5, "connectivity": [5, 2], "materialId": 1}, + {"id": 6, "connectivity": [2, 1], "materialId": 1}, + {"id": 7, "connectivity": [1, 5], "materialId": 1} + ], + "materials": [ + {"id": 1, "youngModulus": 200000000000.0, "area": 1.0} + ] + }, + "displacements":{ + "pin": [ + {"id": 1, "node":4, "angle": 0,"dx": 0, "dy":0} + ], + "rollers": [ + {"id": 1, "node": 1, "direction": 1, "angle": -63.4349, "dx":0} + ] + }, + "forces": [ + {"id": 1, "node":5, "direction": -180,"x": 40000, "y":0}, + {"id": 2, "node":3, "direction": 200,"x": 20000, "y":0} + ] + }""" + + + truss_problem = TrussAnalysisProject.from_json(json_text=JSON_TEXT, info=info) + + truss_problem._forces.update_force_by_id(force_id=1, angle=180+20) + truss_problem._forces.list_forces() + truss_problem.write_input_data() + truss_problem.plot_truss(save=True, show=True) + + # solution + truss_problem.solve() + print("-------------solution ----------------") + truss_problem.report_reactions(fmt='>12.1f') + truss_problem.report_rod_forces(fmt='>12.1f') + # plotting results + truss_problem.plot_deformation(save=True, show=True) + truss_problem.plot_stresses(save=True, show=True) + diff --git a/examples/exam2024-01-json/exam2024-01.json b/examples/exam2024-01-json/exam2024-01.json new file mode 100644 index 0000000..cbfc4de --- /dev/null +++ b/examples/exam2024-01-json/exam2024-01.json @@ -0,0 +1,35 @@ +{ + "mesh":{ + "nodes": [ + {"id": 1, "coordinates": [0, 0]}, + {"id": 2, "coordinates": [0, 2]}, + {"id": 3, "coordinates": [0, 4]}, + {"id": 4, "coordinates": [4, 4]}, + {"id": 5, "coordinates": [4, 2]} + ], + "elements": [ + {"id": 1, "connectivity": [4, 3], "materialId": 1}, + {"id": 2, "connectivity": [3, 2], "materialId": 1}, + {"id": 3, "connectivity": [2, 4], "materialId": 1}, + {"id": 4, "connectivity": [4, 5], "materialId": 1}, + {"id": 5, "connectivity": [5, 2], "materialId": 1}, + {"id": 6, "connectivity": [2, 1], "materialId": 1}, + {"id": 7, "connectivity": [1, 5], "materialId": 1} + ], + "materials": [ + {"id": 1, "youngModulus": 200000000000.0, "area": 1.0} + ] + }, + "displacements":{ + "pin": [ + {"id": 1, "node":4, "angle": 0,"dx": 0, "dy":0} + ], + "rollers": [ + {"id": 1, "node": 1, "direction": 1, "angle": -63.4349, "dx":0} + ] + }, + "forces": [ + {"id": 1, "node":5, "direction": -180,"x": 40000, "y":0}, + {"id": 2, "node":3, "direction": 200,"x": 20000, "y":0} + ] +} \ No newline at end of file diff --git a/examples/exam2024-01-json/exam2024-01_with_project.py b/examples/exam2024-01-json/exam2024-01_with_project.py new file mode 100644 index 0000000..5827212 --- /dev/null +++ b/examples/exam2024-01-json/exam2024-01_with_project.py @@ -0,0 +1,99 @@ +#%% +import pathlib +import numpy as np +import tkinter as tk +from tkinter import filedialog + +from npp_2d_truss_analysis.truss_input import Info, FileData,Mesh,Displacements,Forces, write_input_data +from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis +from npp_2d_truss_analysis.truss_solution import Solution, write_results +from npp_2d_truss_analysis.truss_plotter import TrussPlotter +from npp_2d_truss_analysis.truss_project import TrussAnalysisProject + + +#%% + +pp_project_dir = pathlib.Path('./') +# pp_project_dir = pathlib.Path('../examples/example_101') +FNAME_PREFIX = 'test' + + +info = Info(project_directory=str(pp_project_dir.absolute()), file_name=FNAME_PREFIX) + + +mesh = Mesh() +# mesh.process_mesh(file_data= fileData.mesh) +mesh_json_data ="""{ + "nodes": [ + {"id": 1, "coordinates": [0, 0]}, + {"id": 2, "coordinates": [0, 2]}, + {"id": 3, "coordinates": [0, 4]}, + {"id": 4, "coordinates": [4, 4]}, + {"id": 5, "coordinates": [4, 2]} + ], + "elements": [ + {"id": 1, "connectivity": [4, 3], "materialId": 1}, + {"id": 2, "connectivity": [3, 2], "materialId": 1}, + {"id": 3, "connectivity": [2, 4], "materialId": 1}, + {"id": 4, "connectivity": [4, 5], "materialId": 1}, + {"id": 5, "connectivity": [5, 2], "materialId": 1}, + {"id": 6, "connectivity": [2, 1], "materialId": 1}, + {"id": 7, "connectivity": [1, 0], "materialId": 1} + ], + "materials": [ + {"id": 1, "youngModulus": 200000000000.0, "area": 1.0} + ] +}""" +mesh.process_mesh_json(json_data=mesh_json_data) + + +displacements = Displacements() +displ_json_data ="""{ + "pin": [ + {"id": 1, "node":4, "angle": 0,"dx": 0, "dy":0} + ], + "rollers": [ + {"id": 1, "node": 1, "direction": 1, "angle": -63.4349, "dx":0} + ] +}""" +displacements.process_json(json_data=displ_json_data) +# displacements.process_displacements(file_data= fileData.displacements) + +force_json_data ="""{ + "forces": [ + {"id": 1, "node":5, "direction": -180,"x": 40000, "y":0}, + {"id": 2, "node":1, "direction": 200,"x": 20000, "y":0} + ] +}""" +forces = Forces.from_json_str(force_json_data) + +truss_problem = TrussAnalysisProject(info=info, mesh=mesh, displacements=displacements, forces=forces) + +#%% +# forces.force_components[0] = (80000,0) +forces.list_forces() +#%% +forces.update_force_by_id(force_id=0, fxy=(80000,0)) +#%% + +truss_problem.write_input_data() +truss_problem.solve() +# truss_problem.plot_truss(save=True, show=True) +# %% +# %% +truss_problem.report_reactions(fmt='>12.1f') +truss_problem.report_rod_forces(fmt='>12.1f') + +# %% + + + +m_j = Mesh() +# %% +m_j.process_mesh_json(json_data=json_data) +# %% +import json +data = json.loads(json_data) +# %% +data['nodes'] +# %% diff --git a/examples/exam2024-01-json/exam2024-01_with_project_single_json.py b/examples/exam2024-01-json/exam2024-01_with_project_single_json.py new file mode 100644 index 0000000..8d8c847 --- /dev/null +++ b/examples/exam2024-01-json/exam2024-01_with_project_single_json.py @@ -0,0 +1,51 @@ +#%% +import pathlib +import numpy as np +import tkinter as tk +from tkinter import filedialog + +from npp_2d_truss_analysis.truss_input import Info, FileData,Mesh,Displacements,Forces, write_input_data +from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis +from npp_2d_truss_analysis.truss_solution import Solution, write_results +from npp_2d_truss_analysis.truss_plotter import TrussPlotter +from npp_2d_truss_analysis.truss_project import TrussAnalysisProject + + +#%% + +pp_project_dir = pathlib.Path('./') +JSON_FILE_NAME = pp_project_dir /'exam2024-01.json' +FNAME_PREFIX = 'test' + + +info = Info(project_directory=str(pp_project_dir.absolute()), file_name=FNAME_PREFIX) + +# read the JSON_FILE_NAME file content into json_problem_data string +with open(JSON_FILE_NAME, 'r') as json_file: + json_problem_data = json_file.read() + +mesh = Mesh.from_json(json_data=json_problem_data) + +displacements = Displacements.from_json(json_str=json_problem_data) +# displacements.process_displacements(file_data= fileData.displacements) + +forces = Forces.from_json_str(json_problem_data) +#%% +truss_problem = TrussAnalysisProject(info=info, mesh=mesh, displacements=displacements, forces=forces) + +#%% +# forces.force_components[0] = (80000,0) +forces.list_forces() +#%% +forces.update_force_by_id(force_id=0, fxy=(80000,0)) +#%% + +truss_problem.write_input_data() +truss_problem.solve() +truss_problem.plot_truss(save=True, show=True) +# %% +# %% +truss_problem.report_reactions(fmt='>12.1f') +truss_problem.report_rod_forces(fmt='>12.1f') + +# %% \ No newline at end of file diff --git a/examples/exam2024-01-json/exam2024-01_with_project_single_json_V2.py b/examples/exam2024-01-json/exam2024-01_with_project_single_json_V2.py new file mode 100644 index 0000000..039d62d --- /dev/null +++ b/examples/exam2024-01-json/exam2024-01_with_project_single_json_V2.py @@ -0,0 +1,48 @@ +#%%[markdown] +# # Scope +# This is a version of the trust analysis project that uses the class analysis class with the class method from Json file. +# The main difference from the world other version is that individual mesh displacements and forces objects are not created. +# +# # Notes +# Additionally in this example I am adding the update methods suitable for marking my exam. +# So please disregard any code snippets that have the `am` variable inside +#%% +import pathlib +import numpy as np +import tkinter as tk +from tkinter import filedialog + +from npp_2d_truss_analysis.truss_input import Info, FileData,Mesh,Displacements,Forces, write_input_data +from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis +from npp_2d_truss_analysis.truss_solution import Solution, write_results +from npp_2d_truss_analysis.truss_plotter import TrussPlotter +from npp_2d_truss_analysis.truss_project import TrussAnalysisProject + + +#%% + +pp_project_dir = pathlib.Path('./') +JSON_FILE_NAME = pp_project_dir /'exam2024-01.json' +FNAME_PREFIX = 'test' + +info = Info(project_directory=str(pp_project_dir.absolute()), file_name=FNAME_PREFIX) + +truss_problem = TrussAnalysisProject.from_json_file(json_file_name=JSON_FILE_NAME, info=info) + +#%% +# truss_problem._forces.list_forces() +#%% +am = 20413 +xy =am%100 +truss_problem._forces.update_force_by_id(force_id=1, angle=180+20+xy) +#%% +truss_problem._forces.list_forces() +#%% +truss_problem.write_input_data() +truss_problem.solve() +# truss_problem.plot_truss(save=True, show=True) +print("-------------solution ----------------") +truss_problem.report_reactions(fmt='>12.1f') +truss_problem.report_rod_forces(fmt='>12.1f') + +# %% \ No newline at end of file diff --git a/examples/exam2024-01/ex2_displacements.txt b/examples/exam2024-01/ex2_displacements.txt new file mode 100644 index 0000000..1ca0ae2 --- /dev/null +++ b/examples/exam2024-01/ex2_displacements.txt @@ -0,0 +1,5 @@ +1 +4,0,0,0 +1 +1,1,-63.4349,0 + diff --git a/examples/exam2024-01/ex2_forces.txt b/examples/exam2024-01/ex2_forces.txt new file mode 100644 index 0000000..36324f3 --- /dev/null +++ b/examples/exam2024-01/ex2_forces.txt @@ -0,0 +1,3 @@ +2 +5,-180,40000,0 +3,200,20000,0 diff --git a/examples/exam2024-01/ex2_mesh.txt b/examples/exam2024-01/ex2_mesh.txt new file mode 100644 index 0000000..05808ac --- /dev/null +++ b/examples/exam2024-01/ex2_mesh.txt @@ -0,0 +1,16 @@ +5 +0,0 +0,2 +0,4 +4,4 +4,2 +7 +4,3,1 +3,2,1 +2,4,1 +4,5,1 +5,2,1 +2,1,1 +1,5,1 +1 +200e9,1 diff --git a/examples/exam2024-01/readme.md b/examples/exam2024-01/readme.md new file mode 100644 index 0000000..6fc38d1 --- /dev/null +++ b/examples/exam2024-01/readme.md @@ -0,0 +1,14 @@ +This is an example from the 2024 January exam + +It is almost identical with the ex1, with the difference of the direction of the roller + + +The main tricky bit is the roller direction. + +in this case the +- roller direction is set to one (1) which means fixing the normal direction +- roller angle is set to the direction plane to -63.4349 degrees (atan (4,2)) + +The end result is the same with setting +- the roller direction to 2 (fixing the axis parallel to provided angle) +- setting the angle equal to 23.56 \ No newline at end of file diff --git a/examples/exam2024-01/test_DATA.dat b/examples/exam2024-01/test_DATA.dat new file mode 100644 index 0000000..1e84170 --- /dev/null +++ b/examples/exam2024-01/test_DATA.dat @@ -0,0 +1,35 @@ + NODE COORDINATES +---------------------------------------- +NODE X(M) Y(M) +1 0.000 0.000 +2 0.000 2.000 +3 0.000 4.000 +4 4.000 4.000 +5 4.000 2.000 + + ELEMENTS +---------------------------------------- +EL. NODE1 NODE2 A(M2) E(PA) +1 4 3 1 2E+11 +2 3 2 1 2E+11 +3 2 4 1 2E+11 +4 4 5 1 2E+11 +5 5 2 1 2E+11 +6 2 1 1 2E+11 +7 1 5 1 2E+11 + + PIN SUPPORTS +---------------------------------------- +NODE DX'(M) DY'(M) ANGLE(DEG) +4 0.000 0.000 0.00 + +ROLLER SUPPORTS +---------------------------------------- +NODE DIRECTION DN(M) ANGLE(DEG) +1 1.0 0.000 -63.43 + + FORCES +---------------------------------------- +NODE FX'(N) FY'(N) ANGLE(DEG) +5 80000 0 -180.00 +3 20000 0 200.00 diff --git a/examples/exam2024-01/test_RESULTS.dat b/examples/exam2024-01/test_RESULTS.dat new file mode 100644 index 0000000..09199ce --- /dev/null +++ b/examples/exam2024-01/test_RESULTS.dat @@ -0,0 +1,25 @@ + NODE DISPLACEMENTS +---------------------------------------- +NODE DX(M) DY(M) DM(M) +1 -1.363E-06 2.727E-06 3.049E-06 +2 -1.746E-06 2.727E-06 3.238E-06 +3 -3.759E-07 2.659E-06 2.685E-06 +4 0.000E+00 0.000E+00 0.000E+00 +5 -2.019E-06 3.316E-07 2.047E-06 + + ELEMENT FORCES AND STRESSES +---------------------------------------- +EL. FORCE(N) STRESS(PA) +1 +1.879385E+04 +1.879385E+04 +2 -6.840403E+03 -6.840403E+03 +3 +1.529545E+04 +1.529545E+04 +4 -3.315967E+04 -3.315967E+04 +5 -1.368066E+04 -1.368066E+04 +6 -7.064013E-02 -7.064013E-02 +7 -7.414727E+04 -7.414727E+04 + + SUPPORT REACTIONS +---------------------------------------- +NODE RX(N) RY(N) RM(N) +4 3.247E+04 -2.632E+04 4.180E+04 +1 6.632E+04 3.316E+04 7.415E+04 diff --git a/examples/exam2024-01/test_TRUSS.pdf b/examples/exam2024-01/test_TRUSS.pdf new file mode 100644 index 0000000..c4fc6b7 Binary files /dev/null and b/examples/exam2024-01/test_TRUSS.pdf differ diff --git a/examples/example-np/ex1_displacements.txt b/examples/example-np/ex1_displacements.txt new file mode 100644 index 0000000..560fabb --- /dev/null +++ b/examples/example-np/ex1_displacements.txt @@ -0,0 +1,5 @@ +1 +4,0,0,0 +1 +1,2,26.5651,0 + diff --git a/examples/example-np/ex1_forces.txt b/examples/example-np/ex1_forces.txt new file mode 100644 index 0000000..36324f3 --- /dev/null +++ b/examples/example-np/ex1_forces.txt @@ -0,0 +1,3 @@ +2 +5,-180,40000,0 +3,200,20000,0 diff --git a/examples/example-np/ex1_mesh.txt b/examples/example-np/ex1_mesh.txt new file mode 100644 index 0000000..05808ac --- /dev/null +++ b/examples/example-np/ex1_mesh.txt @@ -0,0 +1,16 @@ +5 +0,0 +0,2 +0,4 +4,4 +4,2 +7 +4,3,1 +3,2,1 +2,4,1 +4,5,1 +5,2,1 +2,1,1 +1,5,1 +1 +200e9,1 diff --git a/examples/example-np/test_DATA.dat b/examples/example-np/test_DATA.dat new file mode 100644 index 0000000..160c496 --- /dev/null +++ b/examples/example-np/test_DATA.dat @@ -0,0 +1,35 @@ + NODE COORDINATES +---------------------------------------- +NODE X(M) Y(M) +1 0.000 0.000 +2 0.000 2.000 +3 0.000 4.000 +4 4.000 4.000 +5 4.000 2.000 + + ELEMENTS +---------------------------------------- +EL. NODE1 NODE2 A(M2) E(PA) +1 4 3 1 2E+11 +2 3 2 1 2E+11 +3 2 4 1 2E+11 +4 4 5 1 2E+11 +5 5 2 1 2E+11 +6 2 1 1 2E+11 +7 1 5 1 2E+11 + + PIN SUPPORTS +---------------------------------------- +NODE DX'(M) DY'(M) ANGLE(DEG) +4 0.000 0.000 0.00 + +ROLLER SUPPORTS +---------------------------------------- +NODE DIRECTION DN(M) ANGLE(DEG) +1 2.0 0.000 26.57 + + FORCES +---------------------------------------- +NODE FX'(N) FY'(N) ANGLE(DEG) +5 40000 0 -180.00 +3 20000 0 200.00 diff --git a/examples/example-np/test_TRUSS.pdf b/examples/example-np/test_TRUSS.pdf new file mode 100644 index 0000000..0079e87 Binary files /dev/null and b/examples/example-np/test_TRUSS.pdf differ diff --git a/examples/example_101/displacements.txt b/examples/example_101/displacements.txt new file mode 100644 index 0000000..0784708 --- /dev/null +++ b/examples/example_101/displacements.txt @@ -0,0 +1,4 @@ +1 +1,0,0,0 +1 +2,1,0,0 \ No newline at end of file diff --git a/examples/example_101/forces.txt b/examples/example_101/forces.txt new file mode 100644 index 0000000..6eeedb3 --- /dev/null +++ b/examples/example_101/forces.txt @@ -0,0 +1,2 @@ +1 +3,0,1000,0 diff --git a/examples/example_101/mesh.txt b/examples/example_101/mesh.txt new file mode 100644 index 0000000..8e676e7 --- /dev/null +++ b/examples/example_101/mesh.txt @@ -0,0 +1,10 @@ +3 +0,0 +1,0 +0,2 +3 +1,2,1 +2,3,1 +3,1,1 +1 +200e9,1 diff --git a/examples/example_101/readme_101.md b/examples/example_101/readme_101.md new file mode 100644 index 0000000..fd31b06 --- /dev/null +++ b/examples/example_101/readme_101.md @@ -0,0 +1,65 @@ +This is the simplest possible truss (3 nodes 3 elements, simply supported) + + +# Mesh details + +The nodes are: + +| Node | X | Y | +| --- | --- | --- | +| 1 | 0 | 0 | +| 2 | 1 | 0 | +| 3 | 0 | 2 | + +The element connectivity is defined in the following table: + +| Element | Node 1 | Node 2 | CrossSection ID| +| --- | --- | --- | --- | +| 1 | 1 | 2 | 1 | +| 2 | 2 | 3 | 1 | +| 3 | 3 | 1 | 1 | + +The mateiral Id is defined in the following table: + +| Crossection ID | E (Pa) | A $m^2$ | +| --- | --- | --- | +| 1 | 2000 | 1 | + +# Displacement boundary conditions + +## Pinned Nodes +The table below presents the pinned nodes: + +| NodeID |Angle | Dx | Dy | +| --- | --- | --- | --- | +| 1 | 0 | 0 | 0 | + + +## Roller Nodes +For the **roller nodes**. The table below presents the roller nodes: + + +| NodeID | Rolling direction | Angle | Distance | +| --- | --- | --- | --- | +| 2 | 1 | 0 | 0 | + + +where: +- Rolling direction: can be either 1(:normal) or 2 (: parallel), and determines if the normal or parallel direction to the plane is **fixed**. +- Angle: is the angle between the global x-axis and the roller support plane. +- Distance: is the normal displacement of the roller support plane. + + +# Forces + +The table below presents the forces: + +| NodeID | Angle | Fx | Fy | +| --- | --- | --- | --- | +| 3 | 0 | 1000 | | + +where: +- NodeId : is the node id where the force is applied. +- Angle: is the angle between the global x-axis and the force direction. +- Fx: is the force in the x direction. +- Fy: is the force in the y direction. diff --git a/examples/example_101/test_DATA.dat b/examples/example_101/test_DATA.dat new file mode 100644 index 0000000..98141d4 --- /dev/null +++ b/examples/example_101/test_DATA.dat @@ -0,0 +1,28 @@ + NODE COORDINATES +---------------------------------------- +NODE X(M) Y(M) +1 0.000 0.000 +2 1.000 0.000 +3 0.000 2.000 + + ELEMENTS +---------------------------------------- +EL. NODE1 NODE2 A(M2) E(PA) +1 1 2 1 2E+11 +2 2 3 1 2E+11 +3 3 1 1 2E+11 + + PIN SUPPORTS +---------------------------------------- +NODE DX'(M) DY'(M) ANGLE(DEG) +1 0.000 0.000 0.00 + +ROLLER SUPPORTS +---------------------------------------- +NODE DIRECTION DN(M) ANGLE(DEG) +2 1.0 0.000 0.00 + + FORCES +---------------------------------------- +NODE FX'(N) FY'(N) ANGLE(DEG) +3 1000 0 0.00 diff --git a/examples/example_101/test_RESULTS.dat b/examples/example_101/test_RESULTS.dat new file mode 100644 index 0000000..5435e11 --- /dev/null +++ b/examples/example_101/test_RESULTS.dat @@ -0,0 +1,19 @@ + NODE DISPLACEMENTS +---------------------------------------- +NODE DX(M) DY(M) DM(M) +1 0.000E+00 0.000E+00 0.000E+00 +2 5.000E-09 0.000E+00 5.000E-09 +3 1.009E-07 2.000E-08 1.029E-07 + + ELEMENT FORCES AND STRESSES +---------------------------------------- +EL. FORCE(N) STRESS(PA) +1 +1.000000E+03 +1.000000E+03 +2 -2.236068E+03 -2.236068E+03 +3 +2.000000E+03 +2.000000E+03 + + SUPPORT REACTIONS +---------------------------------------- +NODE RX(N) RY(N) RM(N) +1 -1.000E+03 -2.000E+03 2.236E+03 +2 0.000E+00 2.000E+03 2.000E+03 diff --git a/examples/example_101/test_TRUSS.pdf b/examples/example_101/test_TRUSS.pdf new file mode 100644 index 0000000..cb772bf Binary files /dev/null and b/examples/example_101/test_TRUSS.pdf differ diff --git a/examples/quickstart/quistart_example.py b/examples/quickstart/quistart_example.py new file mode 100644 index 0000000..bbb16c7 --- /dev/null +++ b/examples/quickstart/quistart_example.py @@ -0,0 +1,85 @@ +#%%[markdown] +# # Scope +# this is a quick start example for the truss analysis package. +# +# # Description +# +# This is a version of the trust analysis project that uses the class analysis class with the class method from Json file. +# The main difference from the world other version is that individual mesh displacements and forces objects are not created. +# +# The problem is based on the exam2024-01 (N. Papadakis lecture course). +# +# # Notes +# Additionally in this example I am adding the update methods suitable for marking my exam. +# So please disregard any code snippets that have the `am` variable inside +#%% +import pathlib +from npp_2d_truss_analysis import Info, TrussAnalysisProject + + +#%% + +pp_project_dir = pathlib.Path('./') +FNAME_PREFIX = 'test' + +info = Info(project_directory=str(pp_project_dir.absolute()), file_name=FNAME_PREFIX) + +JSON_TEXT="""{ + "mesh":{ + "nodes": [ + {"id": 1, "coordinates": [0, 0]}, + {"id": 2, "coordinates": [0, 2]}, + {"id": 3, "coordinates": [0, 4]}, + {"id": 4, "coordinates": [4, 4]}, + {"id": 5, "coordinates": [4, 2]} + ], + "elements": [ + {"id": 1, "connectivity": [4, 3], "materialId": 1}, + {"id": 2, "connectivity": [3, 2], "materialId": 1}, + {"id": 3, "connectivity": [2, 4], "materialId": 1}, + {"id": 4, "connectivity": [4, 5], "materialId": 1}, + {"id": 5, "connectivity": [5, 2], "materialId": 1}, + {"id": 6, "connectivity": [2, 1], "materialId": 1}, + {"id": 7, "connectivity": [1, 5], "materialId": 1} + ], + "materials": [ + {"id": 1, "youngModulus": 200000000000.0, "area": 1.0} + ] + }, + "displacements":{ + "pin": [ + {"id": 1, "node":4, "angle": 0,"dx": 0, "dy":0} + ], + "rollers": [ + {"id": 1, "node": 1, "direction": 1, "angle": -63.4349, "dx":0} + ] + }, + "forces": [ + {"id": 1, "node":5, "direction": -180,"x": 40000, "y":0}, + {"id": 2, "node":3, "direction": 200,"x": 20000, "y":0} + ] +}""" + + +truss_problem = TrussAnalysisProject.from_json(json_text=JSON_TEXT, info=info) + +#%% +# truss_problem._forces.list_forces() +#%% +am = 20413 +xy =am%100 +truss_problem._forces.update_force_by_id(force_id=1, angle=180+20+xy) +#%% +truss_problem._forces.list_forces() +#%% +truss_problem.write_input_data() +truss_problem.solve() +# truss_problem.plot_truss(save=True, show=True) +print("-------------solution ----------------") +truss_problem.report_reactions(fmt='>12.1f') +truss_problem.report_rod_forces(fmt='>12.1f') + +# %% +truss_problem.plot_deformation(save=True, show=True) +truss_problem.plot_stresses(save=True, show=True) +# %% diff --git a/examples/truss_main.py b/examples/truss_main.py new file mode 100644 index 0000000..3b0925b --- /dev/null +++ b/examples/truss_main.py @@ -0,0 +1,56 @@ +#%% +import pathlib +import numpy as np +import tkinter as tk +from tkinter import filedialog + +from npp_2d_truss_analysis.truss_input import Info, FileData,Mesh,Displacements,Forces, write_input_data +from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis +from npp_2d_truss_analysis.truss_solution import Solution, write_results +from npp_2d_truss_analysis.truss_plotter import TrussPlotter + + +pp_project_dir = pathlib.Path('exam2024-01') +pp_project_dir = pathlib.Path('../examples/example_101') +FNAME_PREFIX = 'test' +#%% + +info = Info(project_directory=str(pp_project_dir.absolute()), file_name=FNAME_PREFIX) + +fileData = FileData.from_directory(info.project_directory) +mesh = Mesh() +mesh.process_mesh(file_data= fileData.mesh) + +displacements = Displacements() +displacements.process_displacements(file_data= fileData.displacements) + +forces = Forces() +forces.process_forces(file_data= fileData.forces) +write_input_data(info=info, mesh=mesh, displacements=displacements, forces=forces) + +dofs = Dofs() +dofs.process_dofs(mesh=mesh, displacements=displacements) +analysis = Analysis() +analysis.get_global_stiffness_matrix(mesh=mesh) +analysis.get_global_force_vector(forces=forces, dofs=dofs) +analysis.get_new_displacement_vector(displacements=displacements, dofs=dofs) +analysis.get_new_transformation_matrix(displacements=displacements, dofs=dofs) +#%% +#================== Solution ================== +solution = Solution() +solution.solve_displacement(analysis, dofs) +solution.solve_reaction(displacements=displacements) +solution.solve_stress(mesh=mesh) +# Usage example +# Assume info, mesh, displacements, and solution are instances of their respective classes with attributes set +write_results(info, mesh=mesh, displacements=displacements, solution=solution) + +# %% +tp = TrussPlotter() +tp.get_plot_parameters(mesh=mesh, solution=solution) + +# Usage example +# Assume info, plot, mesh, forces, and displacements are instances of their respective classes with attributes set +tp.plot_truss(info, mesh, forces, displacements, save=True, show=True) + +# %% diff --git a/examples/truss_main_with_project.py b/examples/truss_main_with_project.py new file mode 100644 index 0000000..9e81cd9 --- /dev/null +++ b/examples/truss_main_with_project.py @@ -0,0 +1,78 @@ +#%% +import pathlib +import numpy as np +import tkinter as tk +from tkinter import filedialog + +from npp_2d_truss_analysis.truss_input import Info, FileData,Mesh,Displacements,Forces, write_input_data +from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis +from npp_2d_truss_analysis.truss_solution import Solution, write_results +from npp_2d_truss_analysis.truss_plotter import TrussPlotter +from npp_2d_truss_analysis.truss_project import TrussAnalysisProject + + +#%% + +pp_project_dir = pathlib.Path('exam2024-01') +# pp_project_dir = pathlib.Path('../examples/example_101') +FNAME_PREFIX = 'test' + + +info = Info(project_directory=str(pp_project_dir.absolute()), file_name=FNAME_PREFIX) + +fileData = FileData.from_directory(info.project_directory) +mesh = Mesh() +mesh.process_mesh(file_data= fileData.mesh) + +displacements = Displacements() +displacements.process_displacements(file_data= fileData.displacements) +forces = Forces() +forces.process_forces(file_data= fileData.forces) +truss_problem = TrussAnalysisProject(info=info, mesh=mesh, displacements=displacements, forces=forces) + +#%% +forces.force_components[0] = (80000,0) +#%% + +truss_problem.write_input_data() +truss_problem.solve() +# truss_problem.plot_truss(save=True, show=True) +# %% +# %% +truss_problem.report_reactions(fmt='>12.1f') +truss_problem.report_rod_forces(fmt='>12.1f') + +# %% + + +json_data ="""{ + "nodes": [ + {"id": 1, "coordinates": [0, 0]}, + {"id": 2, "coordinates": [0, 2]}, + {"id": 3, "coordinates": [0, 4]}, + {"id": 4, "coordinates": [4, 4]}, + {"id": 5, "coordinates": [4, 2]} + ], + "elements": [ + {"id": 1, "connectivity": [4, 3], "materialId": 1}, + {"id": 2, "connectivity": [3, 2], "materialId": 1}, + {"id": 3, "connectivity": [2, 4], "materialId": 1}, + {"id": 4, "connectivity": [4, 5], "materialId": 1}, + {"id": 5, "connectivity": [5, 2], "materialId": 1}, + {"id": 6, "connectivity": [2, 1], "materialId": 1}, + {"id": 7, "connectivity": [1, 0], "materialId": 1} + ], + "materials": [ + {"id": 1, "youngModulus": 200000000000.0, "area": 1.0} + ] +}""" + +m_j = Mesh() +# %% +m_j.process_mesh_json(json_data=json_data) +# %% +import json +data = json.loads(json_data) +# %% +data['nodes'] +# %% diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..720f890 --- /dev/null +++ b/src/README.md @@ -0,0 +1,5 @@ +# Truss Analysis + +This is a package for 2d Truss Analysis. + +This is modified by Eduardo Pereira's Matlab Code diff --git a/src/__pycache__/.gitignore b/src/__pycache__/.gitignore new file mode 100644 index 0000000..68bc17f --- /dev/null +++ b/src/__pycache__/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/src/npp_2d_truss_analysis/__init__.py b/src/npp_2d_truss_analysis/__init__.py new file mode 100644 index 0000000..74de736 --- /dev/null +++ b/src/npp_2d_truss_analysis/__init__.py @@ -0,0 +1,10 @@ +""" + @Author: N.Papadakis + @Date: 2024/01/28 +""" +__version__ = '0.1.1' +from npp_2d_truss_analysis.truss_input import Info, FileData,Mesh,Displacements,Forces, write_input_data +from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis +from npp_2d_truss_analysis.truss_solution import Solution, write_results +from npp_2d_truss_analysis.truss_plotter import TrussPlotter +from npp_2d_truss_analysis.truss_project import TrussAnalysisProject \ No newline at end of file diff --git a/src/npp_2d_truss_analysis/truss_analysis_2d.py b/src/npp_2d_truss_analysis/truss_analysis_2d.py new file mode 100644 index 0000000..e087eb1 --- /dev/null +++ b/src/npp_2d_truss_analysis/truss_analysis_2d.py @@ -0,0 +1,302 @@ +#%% +import tkinter as tk +from tkinter import filedialog +import csv +import pathlib +from npp_2d_truss_analysis.truss_input import Info, FileData,Mesh,Displacements,Forces, write_input_data +import numpy as np + + +class Dofs: + def __init__(self): + self.number_dofs = None + self.number_fixed = None + self.number_free = None + self.fixed_dofs = None + self.free_dofs = None + + def process_dofs(self, mesh, displacements): + number_nodes = mesh.number_nodes + number_pin = displacements.number_pin + pin_nodes = displacements.pin_nodes + number_roller = displacements.number_roller + roller_nodes = displacements.roller_nodes + roller_directions = displacements.roller_directions + + number_dofs = 2 * number_nodes + number_fixed_dofs = 2 * number_pin + number_roller + number_free_dofs = number_dofs - number_fixed_dofs + fixed_dofs = [] + + for node in pin_nodes: + fixed_dofs.extend([2 * node - 1, 2 * node]) + + for node, direction in zip(roller_nodes, roller_directions): + if direction == 1: + fixed_dofs.append(2 * node) + else: + fixed_dofs.append(2 * node - 1) + + free_dofs = [dof for dof in range(1, number_dofs + 1) if dof not in fixed_dofs] + + self.number_dofs = number_dofs + self.number_fixed = number_fixed_dofs + self.number_free = number_free_dofs + self.fixed_dofs = fixed_dofs + self.free_dofs = free_dofs + @property + def free_dofs_zero_based(self): + """ + Returns a list of zero-based degrees of freedom (DOFs) for the free DOFs. + """ + return [dof-1 for dof in self.free_dofs] + + @property + def fixed_dofs_zero_based(self): + """ + Returns a list of zero-based degrees of freedom (DOFs) for the fixed DOFs. + """ + return [dof-1 for dof in self.fixed_dofs] + +class Analysis: + # TODO the results of this class needs testing and validation + # against the MAtlab code + + def __init__(self): + self.stiffness_global_matrix = None + self.displacement_new_vector = None + self.force_global_vector = None + self.transformation_new_matrix = None + + def get_global_stiffness_matrix(self, mesh:Mesh): + number_nodes = mesh.number_nodes + number_elements = mesh.number_elements + node_coordinates = mesh.node_coordinates + element_connectivity = mesh.element_connectivity + material_e = mesh.young_modulus + material_a = mesh.area + + k_global = np.zeros((2 * number_nodes, 2 * number_nodes)) + + for i in range(number_elements): + # Element nodes + node1, node2 = element_connectivity[i] + # Element DOFs + element_dofs = [2 * node1 - 1, 2 * node1, 2 * node2 - 1, 2 * node2] + # Element material constants + e = material_e[i] + a = material_a[i] + # Element components and length + dx = node_coordinates[node2-1][0] - node_coordinates[node1-1][0] + dy = node_coordinates[node2-1][1] - node_coordinates[node1-1][1] + l = np.sqrt(dx**2 + dy**2) + # Sine and cosine of angle between reference frames + c = dx / l + s = dy / l + # Global element stiffness matrix + ke = e * a / l * np.array([ + [c * c, c * s, -c * c, -c * s], + [c * s, s * s, -c * s, -s * s], + [-c * c, -c * s, c * c, c * s], + [-c * s, -s * s, c * s, s * s] + ]) + # Assembly + for row_index, dof_i in enumerate(element_dofs): + for col_index, dof_j in enumerate(element_dofs): + k_global[dof_i - 1, dof_j - 1] += ke[row_index, col_index] + + self.stiffness_global_matrix = k_global + + def get_global_force_vector(self, forces:Forces, dofs:Dofs): + """ + Calculates the global force vector based on the given forces and degrees of freedom. + + Args: + forces (Forces): Object containing information about the forces applied to the truss. + dofs (Dofs): Object containing information about the degrees of freedom of the truss. + + Returns: + None + + Modifies: + Updates the `force_global_vector` attribute of the class instance. + """ + number_forces = forces.number_forces + force_nodes = forces.force_nodes + force_components = forces.force_components + force_angles = forces.force_angles + number_dofs = dofs.number_dofs + + f_global = np.zeros(number_dofs) + + for i in range(number_forces): + f_node = force_nodes[i] + f_node_dofs = [2 * f_node - 1, 2 * f_node] + f_comp_xi_yi = np.array(force_components[i]) + f_angle = force_angles[i] + c = np.cos(np.radians(f_angle)) + s = np.sin(np.radians(f_angle)) + t = np.array([[c, s], [-s, c]]) + f_comp_xy = t.T @ f_comp_xi_yi + f_global[f_node_dofs[0] - 1] += f_comp_xy[0] + f_global[f_node_dofs[1] - 1] += f_comp_xy[1] + + self.force_global_vector = f_global + + + def get_new_displacement_vector(self, displacements, dofs): + """Generate a new displacement vector with the known displacements + + #TODO : I don't have a test use case for this method yet + # currently most displacement would be zeros + + Args: + displacements (_type_): _description_ + dofs (_type_): _description_ + """ + number_dofs = dofs.number_dofs + number_pin = displacements.number_pin + pin_nodes = displacements.pin_nodes + pin_displacements = displacements.pin_displacements + pin_angles = displacements.pin_angles + number_roller = displacements.number_roller + roller_nodes = displacements.roller_nodes + roller_directions = displacements.roller_directions + roller_displacements = displacements.roller_displacements + + uc = np.zeros(number_dofs) + + for i in range(number_pin): + p_node = pin_nodes[i] + p_dofs = [2 * p_node - 1, 2 * p_node] + p_displ_xi_yi = np.array(pin_displacements[i]) + p_angle = pin_angles[i] + c = np.cos(np.radians(p_angle)) + s = np.sin(np.radians(p_angle)) + t = np.array([[c, s], [-s, c]]) + p_displ_xy = t.T @ p_displ_xi_yi + uc[p_dofs[0] - 1] = p_displ_xy[0] + uc[p_dofs[1] - 1] = p_displ_xy[1] + + for i in range(number_roller): + r_node = roller_nodes[i] + r_direction = roller_directions[i] + r_displacement = roller_displacements[i] + r_dof = 2 * r_node if r_direction == 1 else 2 * r_node - 1 + uc[r_dof - 1] = r_displacement + + self.displacement_new_vector = uc + + + def get_new_transformation_matrix(self, displacements:Displacements, dofs:Dofs): + number_dofs = dofs.number_dofs + number_roller = displacements.number_roller + roller_nodes = displacements.roller_nodes + roller_angles = displacements.roller_angles + + tc = np.eye(number_dofs) + + for i in range(number_roller): + r_node = roller_nodes[i] + r_dofs = [2 * r_node - 2, 2 * r_node-1] + r_angle = roller_angles[i] + c = np.cos(np.radians(r_angle)) + s = np.sin(np.radians(r_angle)) + t = np.array([[c, s], [-s, c]]) + tc[np.ix_(r_dofs, r_dofs)] = t + + self.transformation_new_matrix = tc + + +#%% +if __name__ == '__main__': + pp_project_dir = pathlib.Path('example-np') + info = Info(project_directory=str(pp_project_dir.absolute()), file_name='test') + # info.get_project_info() + print(info.project_directory) + print(info.file_name) + # %% + fileData = FileData.from_directory(info.project_directory) + # %% + print(fileData.mesh) + print(fileData.displacements) + # %% + # Usage + mesh = Mesh() + mesh.process_mesh(file_data= fileData.mesh) + + # Now mesh instance has its attributes set based on file data + # %% + print("Number of nodes: ", mesh.number_nodes) + print("Number of elements: ", mesh.number_elements) + print("Node coordinates: ", mesh.node_coordinates) + print("Element connectivity: ", mesh.element_connectivity) + print("Young's modulus: ", mesh.young_modulus) + print("Area: ", mesh.area) + + # %% + # Usage + displacements = Displacements() + displacements.process_displacements(file_data= fileData.displacements) + + print(f"Number of pin: {displacements.number_pin}") + print(f"Pin nodes: {displacements.pin_nodes}") + print(f"Pin angles: {displacements.pin_angles}") + print(f"Pin displacements: {displacements.pin_displacements}") + print(f"Number of roller: {displacements.number_roller}") + print(f"Roller nodes: {displacements.roller_nodes}") + print(f"Roller directions: {displacements.roller_directions}") + print(f"Roller angles: {displacements.roller_angles}") + print(f"Roller displacements: {displacements.roller_displacements}") + print(f"Number of support: {displacements.number_support}") + + # %% + # Usage + forces = Forces() + forces.process_forces(file_data= fileData.forces) + print(f"Number of forces: {forces.number_forces}") + print(f"Force nodes: {forces.force_nodes}") + print(f"Force angles: {forces.force_angles}") + print(f"Force components: {forces.force_components}") + + # %% + # Usage example + # Assume info, mesh, displacements, and forces are instances of their respective classes with attributes set + write_input_data(info=info, mesh=mesh, displacements=displacements, forces=forces) + + # %% + + # Usage + dofs = Dofs() + # Assume mesh and displacements are instances of their respective classes with attributes set + dofs.process_dofs(mesh=mesh, displacements=displacements) + print(f"Number of DOFs: {dofs.number_dofs}") + print(f"Number of fixed DOFs: {dofs.number_fixed}") + print(f"Number of free DOFs: {dofs.number_free}") + print(f"Fixed DOFs: {dofs.fixed_dofs}") + print(f"Free DOFs: {dofs.free_dofs}") + + # %% + # Usage + analysis = Analysis() + # Assume mesh is an instance of the Mesh class with attributes set + analysis.get_global_stiffness_matrix(mesh=mesh) + analysis.get_global_force_vector(forces=forces, dofs=dofs) + print(analysis.stiffness_global_matrix) + print(analysis.stiffness_global_matrix.shape) + print(analysis.stiffness_global_matrix.dtype) + # %% + print(analysis.force_global_vector) + print(analysis.force_global_vector.shape) + print(analysis.force_global_vector.dtype) + # %% + analysis.get_new_displacement_vector(displacements=displacements, dofs=dofs) + print("New Displacement vector==============================") + print(analysis.displacement_new_vector) + print(analysis.displacement_new_vector.shape) + # %% + analysis.get_new_transformation_matrix(displacements=displacements, dofs=dofs) + print("New Transformation matrix==============================") + print(analysis.transformation_new_matrix) + print(f"New Transformation matrix shape: {analysis.transformation_new_matrix.shape}") + print(f"New Transformation matrix dtype: {analysis.transformation_new_matrix.dtype}") diff --git a/src/npp_2d_truss_analysis/truss_input.py b/src/npp_2d_truss_analysis/truss_input.py new file mode 100644 index 0000000..bf82f8e --- /dev/null +++ b/src/npp_2d_truss_analysis/truss_input.py @@ -0,0 +1,408 @@ +#%% +import logging +import csv +import json +import pathlib + +import numpy as np +import tkinter as tk +from tkinter import filedialog + +logging.basicConfig(level=logging.DEBUG) + +def read_file(file_name, file_path): + file_data = [] + + with open(file_path / file_name, mode='r') as file: + reader = csv.reader(file) + for row in reader: + # Convert each value in the row from string to float + numeric_row = [float(value) for value in row] + file_data.append(numeric_row) + + return file_data + +class Info: + def __init__(self, project_directory:str=None, file_name:str=None): + self.project_directory = project_directory + self.file_name = file_name + + def get_project_info(self): + # Set up the root Tkinter window + root = tk.Tk() + root.withdraw() # Hide the main window + + # Graphical directory selection + project_dir = filedialog.askdirectory(title='Select Project Folder') + self.project_directory = project_dir + + # Console input for file name + file_name = input('Output files name: ') + self.file_name = file_name + +class FileData: + def __init__(self, mesh=None, displacements=None, forces=None): + self.mesh = mesh + self.displacements = displacements + self.forces = forces + + @staticmethod + def read_file_text(file_path): + return file_path.read_text() + + @staticmethod + def read_file(file_path:pathlib.Path): + file_data = [] + logging.debug(f'Reading file: {file_path.absolute()}') + with open(str(file_path.absolute()), mode='r') as file: + reader = csv.reader(file) + for row in reader: + logging.debug(f'Reading row: {row}') + # Convert each value in the row from string to float + numeric_row = [float(value) for value in row] + file_data.append(numeric_row) + + return file_data + + @classmethod + def from_directory(cls, directory): + directory_path = pathlib.Path(directory) + + # Initialize file paths as None + mesh_path = displacements_path = forces_path = None + + # Search for specific files in the directory + for file_path in directory_path.iterdir(): + if file_path.is_file(): + if 'mesh' in file_path.name: + logging.debug(f'Found MESH file: {directory_path/file_path.name}') + mesh_path = directory_path/file_path + elif 'displacements' in file_path.name: + logging.debug(f'Found DISPLACEMENTS file: {directory_path/file_path.name}') + displacements_path = directory_path/file_path + elif 'forces' in file_path.name: + logging.debug(f'Found FORCES file: {directory_path/file_path.name}') + forces_path = directory_path/file_path + + # Read the contents of the files, if found + mesh = cls.read_file(mesh_path) if mesh_path else None + displacements = cls.read_file(displacements_path) if displacements_path else None + forces = cls.read_file(forces_path) if forces_path else None + + # Return an instance of FileData with the contents + return cls(mesh, displacements, forces) + +class Mesh: + def __init__(self): + self.number_nodes = None + self.number_elements = None + self.node_coordinates = None + self.element_connectivity = None + self.young_modulus = None + self.area = None + + def process_mesh(self, file_data:np.ndarray): + file_line = 0 + + # Process node data + self.number_nodes = int(file_data[file_line][0]) + file_line += 1 + self.node_coordinates = [] + for _ in range(self.number_nodes): + self.node_coordinates.append((file_data[file_line][0], file_data[file_line][1])) + file_line += 1 + + # Process element data + self.number_elements = int(file_data[file_line][0]) + file_line += 1 + self.element_connectivity = [] + element_material = [] + for _ in range(self.number_elements): + self.element_connectivity.append((int(file_data[file_line][0]), int(file_data[file_line][1]))) + element_material.append(int(file_data[file_line][2])) + file_line += 1 + + # Process material data + number_materials = int(file_data[file_line][0]) + file_line += 1 + material_e = [] + material_a = [] + for _ in range(number_materials): + material_e.append(file_data[file_line][0]) + material_a.append(file_data[file_line][1]) + file_line += 1 + + # Assigning Young's modulus and area to elements + self.young_modulus = [material_e[mat - 1] for mat in element_material] + self.area = [material_a[mat - 1] for mat in element_material] + + def process_mesh_json(self, json_data:str): + # Parse JSON data + data = json.loads(json_data) + + try: + #This will happer if a single JSON file for the entire problem is used + data = data["mesh"] + except: + pass + finally: + assert data.get("nodes", None) is not None, "Missing mesh data" + + # Process node data + self.node_coordinates = [(node["coordinates"][0], node["coordinates"][1]) for node in data["nodes"]] + self.number_nodes = len(data["nodes"]) + # Process element data + self.number_elements = len(data["elements"]) + self.element_connectivity = [(element["connectivity"][0], element["connectivity"][1]) for element in data["elements"]] + element_material = [element["materialId"] for element in data["elements"]] + + # Process material data + material_dict = {material["id"]: (material["youngModulus"], material["area"]) for material in data["materials"]} + + # Assigning Young's modulus and area to elements + self.young_modulus = [material_dict[mat][0] for mat in element_material] + self.area = [material_dict[mat][1] for mat in element_material] + + @classmethod + def from_json(cls, json_data:str): + mesh = cls() + mesh.process_mesh_json(json_data) + return mesh + +class Displacements: + def __init__(self): + self.number_pin = None + self.pin_nodes = None + self.pin_displacements = None + self.pin_angles = None + self.number_roller = None + self.roller_nodes = None + self.roller_directions = None + self.roller_angles = None + self.roller_displacements = None + self.number_support = None + self.support_nodes = None + + def process_displacements(self, file_data): + file_line = 0 + + # Process pin data + self.number_pin = int(file_data[file_line][0]) + file_line += 1 + self.pin_nodes = [] + self.pin_angles = [] + self.pin_displacements = [] + for _ in range(self.number_pin): + self.pin_nodes.append(int(file_data[file_line][0])) + self.pin_angles.append(file_data[file_line][1]) + self.pin_displacements.append((file_data[file_line][2], file_data[file_line][3])) + file_line += 1 + + # Process roller data + self.number_roller = int(file_data[file_line][0]) + file_line += 1 + self.roller_nodes = [] + self.roller_directions = [] + self.roller_angles = [] + self.roller_displacements = [] + for _ in range(self.number_roller): + self.roller_nodes.append(int(file_data[file_line][0])) + self.roller_directions.append(file_data[file_line][1]) + self.roller_angles.append(file_data[file_line][2]) + self.roller_displacements.append(file_data[file_line][3]) + file_line += 1 + + # Process support data + self.number_support = self.number_pin + self.number_roller + self.support_nodes = self.pin_nodes + self.roller_nodes + + def process_json(self, json_data:str): + # Parse JSON data + data = json.loads(json_data) + try: + #This will happer if a single JSON file for the entire problem is used + data = data["displacements"] + except: + pass + finally: + assert data.get("pin", None) is not None, "Missing displacements pin data section" + assert data.get("rollers", None) is not None, "Missing displacements roller data section" + + + # Process pin data + self.number_pin = len(data["pin"]) + self.pin_nodes = [pin["node"] for pin in data["pin"]] + self.pin_angles = [pin["angle"] for pin in data["pin"]] + self.pin_displacements = [(pin["dx"], pin["dy"]) for pin in data["pin"]] + + + # Process roller data + self.roller_nodes = [roller["node"] for roller in data["rollers"]] + self.roller_directions = [roller["direction"] for roller in data["rollers"]] + self.roller_angles = [roller["angle"] for roller in data["rollers"]] + self.roller_displacements = [roller["dx"] for roller in data["rollers"]] + self.number_roller = len(self.roller_nodes) + + # Process support data + self.number_support = self.number_pin + self.number_roller + self.support_nodes = self.pin_nodes + self.roller_nodes + + @classmethod + def from_json(cls, json_str:str): + displacements = cls() + displacements.process_json(json_str) + return displacements + +class Forces: + def __init__(self): + self.number_forces = None + self.force_nodes = None + self.force_components = None + self.force_angles = None + + def process_forces(self, file_data:np.ndarray): + file_line = 0 + + # Process forces data + self.number_forces = int(file_data[file_line][0]) + file_line += 1 + self.force_ids = range(self.number_forces) + self.force_nodes = [] + self.force_angles = [] + self.force_components = [] + for _ in range(self.number_forces): + self.force_nodes.append(int(file_data[file_line][0])) + self.force_angles.append(file_data[file_line][1]) + self.force_components.append((file_data[file_line][2], file_data[file_line][3],)) + file_line += 1 + + def process_json(self, json_str:str): + """ + Process the JSON data and extract forces information. + + Args: + json_data (str): JSON data containing forces information. + + Returns: + None + """ + # Parse JSON data + data = json.loads(json_str) + # Process forces data + self.number_forces = len(data["forces"]) + self.force_ids = range(self.number_forces) + self.force_nodes = [force["node"] for force in data["forces"]] + self.force_angles = [force["direction"] for force in data["forces"]] + self.force_components = [(force["x"], force["y"]) for force in data["forces"]] + + def list_forces(self, force_fmt:str='^10.2f'): + """ + Prints the list of forces applied to the truss nodes. + + Each force is displayed with its corresponding node, force components, and angle. + """ + print(f'| ID | Node: | Angle [deg] |({"FX":^10s},{"FY":^10s})') + print(f'|{"-"*10}|{"-"*10}|{"-"*13}|{"-"*10}-{"-"*10}') + for id, node, (fx, fy), angle in zip(range(self.number_forces), self.force_nodes, self.force_components, self.force_angles): + print(f'|{id:^10d}|{node:^10d}|{angle:^13.2f}|({format(fx, force_fmt)},{format(fy, force_fmt)})') + + def get_force_by_id(self, id) -> dict: + """ + Get the force information for a given force ID + + Args: + id (int): Force ID + + Returns: + dict: Dictionary with the force information. + """ + if id<0 or id>=self.number_forces or not isinstance(id, int): + raise ValueError(f'Invalid force ID: {id}') + return {'id': id, 'node': self.force_nodes[id], 'fxy': self.force_components[id], 'angle': self.force_angles[id]} + + + def update_force_by_id(self, force_id:int, node:int=None, fxy:tuple=None, angle:float=None): + """Update the force information for a given force ID + """ + if force_id<0 or force_id>=self.number_forces or not isinstance(force_id, int): + raise ValueError(f'Invalid force ID: {force_id}') + if node is not None: + self.force_nodes[force_id] = node + if fxy is not None: + assert isinstance(fxy, tuple) and all([isinstance(f, (int, float)) for f in fxy ]) and len(fxy)==2, 'fxy should be a tuple with (float, float)' + self.force_components[force_id] = fxy + if angle is not None: + self.force_angles[force_id] = angle + + @classmethod + def from_json_str(cls, json_str:str): + """ + Create a Forces instance from JSON data. + + Args: + json_str (str): JSON data containing forces information. + + Returns: + Forces: Forces instance with the forces information. + """ + forces = cls() + forces.process_json(json_str) + return forces + +def write_input_data(info, mesh, displacements, forces): + """Write input data for the problem to a file + + Includes mesh, Displacements, and Forces data + """ + project_dir = info.project_directory + file_name = info.file_name + new_file_name = f"{file_name}_DATA.dat" + file_path = f"{project_dir}/{new_file_name}" + + with open(file_path, 'w') as file: + bar_line = '-' * 40 + + # Writing node coordinates + file.write(' NODE COORDINATES\n') + file.write(f'{bar_line}\n') + file.write('NODE X(M) Y(M)\n') + for i, (x, y) in enumerate(mesh.node_coordinates, start=1): + file.write(f'{i:<4} {x:11.3f} {y:11.3f}\n') + file.write('\n') + + # Writing elements + file.write(' ELEMENTS\n') + file.write(f'{bar_line}\n') + file.write('EL. NODE1 NODE2 A(M2) E(PA)\n') + for i, ((node1, node2), a, e) in enumerate(zip(mesh.element_connectivity, mesh.area, mesh.young_modulus), start=1): + file.write(f'{i:<3} {node1:<6} {node2:<6} {a:11.6G} {e:10.5G}\n') + file.write('\n') + + # Writing pin supports + if displacements.number_pin > 0: + file.write(' PIN SUPPORTS\n') + file.write(f'{bar_line}\n') + file.write('NODE DX\'(M) DY\'(M) ANGLE(DEG)\n') + for node, (dx, dy), angle in zip(displacements.pin_nodes, displacements.pin_displacements, displacements.pin_angles): + file.write(f'{node:<4} {dx:11.3f} {dy:11.3f} {angle:11.2f}\n') + file.write('\n') + + # Writing roller supports + if displacements.number_roller > 0: + file.write('ROLLER SUPPORTS\n') + file.write(f'{bar_line}\n') + file.write('NODE DIRECTION DN(M) ANGLE(DEG)\n') + for node, direction, dn, angle in zip(displacements.roller_nodes, displacements.roller_directions, displacements.roller_displacements, displacements.roller_angles): + file.write(f'{node:<4} {direction:<11} {dn:11.3f} {angle:11.2f}\n') + file.write('\n') + + # Writing forces + file.write(' FORCES\n') + file.write(f'{bar_line}\n') + file.write('NODE FX\'(N) FY\'(N) ANGLE(DEG)\n') + for node, (fx, fy), angle in zip(forces.force_nodes, forces.force_components, forces.force_angles): + file.write(f'{node:<4} {fx:11.6G} {fy:11.6G} {angle:11.2f}\n') + + + +# %% diff --git a/src/npp_2d_truss_analysis/truss_plotter.py b/src/npp_2d_truss_analysis/truss_plotter.py new file mode 100644 index 0000000..a373951 --- /dev/null +++ b/src/npp_2d_truss_analysis/truss_plotter.py @@ -0,0 +1,578 @@ +#%% +import pathlib +import numpy as np +import tkinter as tk +from tkinter import filedialog + +import matplotlib.pyplot as plt + +from npp_2d_truss_analysis.truss_input import Info, FileData,Mesh,Displacements,Forces, write_input_data +from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis +from npp_2d_truss_analysis.truss_solution import Solution, write_results + + +def get_colors(current_value, positive_value, negative_value): + color_compression = np.array([0.7843, 0, 0.1569]) + color_zero = np.array([0.9020, 0.9020, 0.9020]) + color_tension = np.array([0.1569, 0, 0.7843]) + + if current_value < 0: + ratio = current_value / negative_value + c1 = color_compression + c2 = color_zero + else: + ratio = current_value / positive_value + c1 = color_tension + c2 = color_zero + + element_color = np.zeros(3) + element_color[1] = c2[1] + ratio * (c1[1] - c2[1]) + element_color[0] = c2[0] + ratio * (c1[0] - c2[0]) + element_color[2] = c2[2] + ratio * (c1[2] - c2[2]) + + return element_color + +def get_roller_lines(app_point, dir_vector, plot_scale): + """returns the + + Args: + app_point (list two elemetns): list element + dir_vector (int): 1-normal, 2-parallel + plot_scale (float): plot scale + + Returns: + list: [x1,y1, x2,y2] end coordinates of roller + """ + roller_l_paper = 16 # mm + roller_h_paper = 2.5 # mm + + roller_l = roller_l_paper / 10 / plot_scale # converting to meters + roller_h = roller_h_paper / 10 / plot_scale # converting to meters + + d_dir = dir_vector + n_dir = np.array([[0, -1], [1, 0]]) @ d_dir + + point_o = app_point + point_a1 = point_o + (n_dir * roller_h / 2) - (d_dir * roller_l / 2) + point_b1 = point_o + (n_dir * roller_h / 2) + (d_dir * roller_l / 2) + point_a2 = point_o - (n_dir * roller_h / 2) - (d_dir * roller_l / 2) + point_b2 = point_o - (n_dir * roller_h / 2) + (d_dir * roller_l / 2) + + draw_seg = np.array([point_a1, point_b1, point_a2, point_b2]) + return draw_seg + + +def get_force_arrow(app_point, dir_vector, vector_length, plot_scale): + arrow_l_paper = 3 # mm + arrow_w_paper = 2 # mm + vector_l_paper = vector_length # mm + + arrow_l = arrow_l_paper / 10 / plot_scale # converting to meters + arrow_w = arrow_w_paper / 10 / plot_scale # converting to meters + vector_l = vector_l_paper / 10 / plot_scale # converting to meters + + d_dir = dir_vector + n_dir = np.array([[0, -1], [1, 0]]) @ d_dir + + point_o = app_point + point_c = point_o + d_dir * vector_l + point_a = point_c - (d_dir * arrow_l) + (n_dir * arrow_w / 2) + point_b = point_c - (d_dir * arrow_l) - (n_dir * arrow_w / 2) + + draw_seg = np.array([point_o, point_c, point_a, point_c, point_b, point_c]) + return draw_seg + +class TrussPlotter: + def __init__(self): + self.plot_x_limits = None + self.plot_y_limits = None + self.paper_size = None + self.paper_position = None + self.plot_scale = None + self.scale_factor = None + + def get_plot_parameters(self, mesh:Mesh, solution:Solution=None): + node_coordinates = np.array(mesh.node_coordinates) + # node_displacements = np.array(solution.global_displacements) if Solution is not None else np.zeros_like(node_coordinates) + node_displacements_max = solution.get_max_displacement() if solution is not None else 0 + + # Node coordinates + nc_x = node_coordinates[:, 0] + nc_y = node_coordinates[:, 1] + + # Truss limits + tx_min = np.min(nc_x) + tx_max = np.max(nc_x) + ty_min = np.min(nc_y) + ty_max = np.max(nc_y) + + # Truss size + tx_size = tx_max - tx_min + ty_size = ty_max - ty_min + + # Margins + margins = 0.09 * max(tx_size, ty_size) + + # Plot limits + px_min = tx_min - margins + px_max = tx_max + margins + py_min = ty_min - margins + py_max = ty_max + margins + plot_x_limits = [px_min, px_max] + plot_y_limits = [py_min, py_max] + + # Axis size + ax_size = px_max - px_min + ay_size = py_max - py_min + + # Figure size + fXSize = ax_size / 0.84 + fYSize = ay_size / 0.88 + + # Figure size and paper orientation + if fXSize > fYSize: + paper_size = [29.7, 21.0] # A4 landscape + if fXSize / fYSize > 297 / 210: + plot_scale = 29.7 / fXSize + else: + plot_scale = 21.0 / fYSize + else: + paper_size = [21.0, 29.7] # A4 portrait + if fXSize / fYSize > 210 / 297: + plot_scale = 21.0 / fXSize + else: + plot_scale = 29.7 / fYSize + paper_position = [0, 0] + paper_size + + # Deformed scale factor + max_displ = np.max(np.abs(node_displacements_max)) + scale_factor = margins * 0.5 / max_displ if max_displ != 0 else 0 + + self.plot_x_limits = plot_x_limits + self.plot_y_limits = plot_y_limits + self.paper_size = paper_size + self.paper_position = paper_position + self.plot_scale = plot_scale + self.scale_factor = scale_factor + + + def plot_truss(self, info, mesh, forces, displacements, save:bool=True, show:bool=True): + project_dir = info.project_directory + file_name = info.file_name + number_nodes = mesh.number_nodes + number_elements = mesh.number_elements + node_coordinates = np.array(mesh.node_coordinates) + element_connectivity = mesh.element_connectivity + force_nodes = forces.force_nodes + pin_nodes = displacements.pin_nodes + number_roller = displacements.number_roller + roller_nodes = displacements.roller_nodes + roller_directions = displacements.roller_directions + roller_angles = displacements.roller_angles + + new_file_name = f"{file_name}_TRUSS.pdf" + file_path = f"{project_dir}/{new_file_name}" + + # Node number text real displacement + d_x_real = 0.14 / self.plot_scale # meters + d_y_real = 0.5 / self.plot_scale # meters + + # Create figure and axes + fig, ax = plt.subplots( + figsize=(self.paper_size[0]/2.54, self.paper_size[1]/2.54)) # Size in inches + ax.set_xlim(self.plot_x_limits) + ax.set_ylim(self.plot_y_limits) + ax.set_title(f"{file_name}: Truss") + ax.set_xlabel('x [m]') + ax.set_ylabel('y [m]') + ax.set_aspect('equal', adjustable='box') + + # plot rollers + self._plot_rollers(node_coordinates, displacements, ax) + + # Plot elements + for i in range(number_elements): + node1, node2 = element_connectivity[i] + x_coords = [node_coordinates[node1-1][0], node_coordinates[node2-1][0]] + y_coords = [node_coordinates[node1-1][1], node_coordinates[node2-1][1]] + ax.plot(x_coords, y_coords, linestyle='-', linewidth=2.5, color='grey') + + # plot forces + self._plot_force_vectors(node_coordinates, forces, ax) + + # % Nodes + self._plot_all_nodes(mesh= mesh, displacements=displacements, ax=ax) + + # Force nodes + force_nodes_adjusted = [fn - 1 for fn in force_nodes] + force_x_coord = node_coordinates[force_nodes_adjusted, 0] + force_y_coord = node_coordinates[force_nodes_adjusted, 1] + ax.plot(force_x_coord, force_y_coord, linestyle='none', linewidth=0.1, + marker='o', markersize=1.3, markeredgecolor='black', + markerfacecolor='black') + + # Plot node numbers + for i in range(number_nodes): + x, y = node_coordinates[i] + ax.text(x + d_x_real, y + d_y_real, str(i+1), fontsize=6, + ha='center', va='center', backgroundcolor='white') + + # Plot element numbers + self._plot_element_numbers(mesh, ax) + + # Save plot as PDF + if save: + plt.savefig(file_path) + if show: + plt.show() + plt.close() + + def _plot_element_numbers(self, mesh:Mesh, ax): + element_connectivity = mesh.element_connectivity + number_elements = mesh.number_elements + node_coordinates = np.array(mesh.node_coordinates) + + for i in range(number_elements): + node1, node2 = element_connectivity[i] + mid_point = (node_coordinates[node1-1] + node_coordinates[node2-1]) / 2 + ax.text(mid_point[0], mid_point[1], str(i+1), fontsize=6, + ha='center', va='center', backgroundcolor='white') + + def _plot_all_nodes(self, mesh:Mesh, displacements:Displacements, ax): + node_coordinates = np.array(mesh.node_coordinates) + pin_nodes = displacements.pin_nodes + roller_nodes = displacements.roller_nodes + + node_x_coord = node_coordinates[:, 0] + node_y_coord = node_coordinates[:, 1] + ax.plot(node_x_coord, node_y_coord, linestyle='none', linewidth=1, + marker='o', markersize=14, markeredgecolor='black', + markerfacecolor='white') + + # % Pin nodes + pin_nodes_adjusted = [pn - 1 for pn in pin_nodes] + + # Pin Nodes + pin_x_coord = node_coordinates[pin_nodes_adjusted, 0] + pin_y_coord = node_coordinates[pin_nodes_adjusted, 1] + ax.plot(pin_x_coord, pin_y_coord, linestyle='none', linewidth=1.5, + marker='o', markersize=5, markeredgecolor='black', + markerfacecolor='black') + + # Roller nodes + roller_nodes_adjusted = [rn - 1 for rn in roller_nodes] + # Roller Nodes + roller_x_coord = node_coordinates[roller_nodes_adjusted, 0] + roller_y_coord = node_coordinates[roller_nodes_adjusted, 1] + ax.plot(roller_x_coord, roller_y_coord, linestyle='none', linewidth=1.5, + marker='o', markersize=5, markeredgecolor='black', + markerfacecolor='white') + + def _plot_force_vectors(self, node_coordinates, + forces:Forces, ax): + number_forces = forces.number_forces + force_nodes = forces.force_nodes + force_components = np.array(forces.force_components) + force_angles = forces.force_angles + for num_for in range(number_forces): + f_node = force_nodes[num_for] - 1 # Adjust for zero-based indexing in Python + f_comp_xi_yi = force_components[num_for, :] + f_angle = force_angles[num_for] + c = np.cos(np.radians(f_angle)) + s = np.sin(np.radians(f_angle)) + t = np.array([[c, s], [-s, c]]) + f_comp_xy = t.T @ f_comp_xi_yi + f_dir_vec = f_comp_xy / np.linalg.norm(f_comp_xy) + + draw_seg = get_force_arrow(node_coordinates[f_node,:], f_dir_vec, 14, self.plot_scale) + for seg in range(3): + ax.plot(draw_seg[2*seg:2*seg+2, 0], draw_seg[2*seg:2*seg+2, 1], + linestyle='-', linewidth=1.3, marker='none', color='black') + + def _plot_rollers(self, node_coordinates, displacements:Displacements, ax): + number_roller = displacements.number_roller + roller_nodes = displacements.roller_nodes + roller_directions = displacements.roller_directions + roller_angles = displacements.roller_angles + + for num_rol in range(number_roller): + r_node = roller_nodes[num_rol] - 1 # Adjust for zero-based indexing in Python + r_direction = roller_directions[num_rol] + r_angle = roller_angles[num_rol] + c = np.cos(np.radians(r_angle)) + s = np.sin(np.radians(r_angle)) + + r_dir_vec = np.array([c, s]) if r_direction == 1 else np.array([-s, c]) + + draw_seg = get_roller_lines(node_coordinates[r_node,:], r_dir_vec, self.plot_scale) + for seg in range(2): + ax.plot(draw_seg[2*seg:2*seg+2, 0], draw_seg[2*seg:2*seg+2, 1], + linestyle='-', linewidth=1, marker='none', color='black') + + + def plot_deformation(self, info:Info, mesh:Mesh, forces:Forces, + displacements:Displacements, solution:Solution, + save:bool=True, show:bool=True): + # TODO some lists need to be converted to arrays. + project_dir = info.project_directory + file_name = info.file_name + # paper_position = self.paper_position + number_nodes = mesh.number_nodes + number_elements = mesh.number_elements + node_coordinates = np.transpose(np.array(mesh.node_coordinates)) + element_connectivity = np.array(mesh.element_connectivity) + number_pin = displacements.number_pin + pin_nodes = np.array(displacements.pin_nodes) + number_roller = displacements.number_roller + roller_nodes = np.array(displacements.roller_nodes) + node_real_displacements = solution.global_displacements + + new_file_name = f"{file_name}_DEFORMATION.pdf" + file_path = f"{project_dir}/{new_file_name}" + + node_displacements = node_real_displacements * self.scale_factor + + # Figure and axes setup + fig, ax = plt.subplots() + fig.set_size_inches(self.paper_size[0] / 2.54, self.paper_size[1] / 2.54) # Convert cm to inches + ax.set_xlim(self.plot_x_limits) + ax.set_ylim(self.plot_y_limits) + ax.set_aspect('equal', adjustable='box') + ax.set_title(f"{file_name}: Deformation (x{self.scale_factor:.2E})") + ax.set_xlabel('x [m]') + ax.set_ylabel('y [m]') + + # Undeformed elements + for num_ele in range(number_elements): + node1, node2 = element_connectivity[num_ele,:] + x1_coord, y1_coord = node_coordinates[:,node1 - 1] + x2_coord, y2_coord = node_coordinates[:, node2 - 1] + ax.plot([x1_coord, x2_coord], [y1_coord, y2_coord], linestyle='--', linewidth=1, color=[0.6, 0.6, 0.6]) + + # Undeformed nodes + node_x_coord = node_coordinates[0, :] + node_y_coord = node_coordinates[1, :] + ax.plot(node_x_coord, node_y_coord, linestyle='none', linewidth=1, marker='o', markersize=3, markeredgecolor=[0.0, 0.0, 0.0], markerfacecolor=[1.0, 1.0, 1.0]) + + # Undeformed pin nodes + pin_x_coord = node_coordinates[0, pin_nodes - 1] # Adjust for zero-based indexing + pin_y_coord = node_coordinates[1, pin_nodes - 1] + ax.plot(pin_x_coord, pin_y_coord, linestyle='none', linewidth=1.5, marker='o', markersize=3, markeredgecolor=[0.0, 0.0, 0.0], markerfacecolor=[0.0, 0.0, 0.0]) + + # Undeformed roller nodes + roller_x_coord = node_coordinates[0, roller_nodes - 1] # Adjust for zero-based indexing + roller_y_coord = node_coordinates[1, roller_nodes - 1] + ax.plot(roller_x_coord, roller_y_coord, linestyle='none', linewidth=1.5, marker='o', markersize=3, markeredgecolor=[0.0, 0.0, 0.0], markerfacecolor=[1.0, 1.0, 1.0]) + + # Deformed elements + for num_ele in range(number_elements): + node1, node2 = element_connectivity[num_ele,:] + x1_coord, y1_coord = node_coordinates[:, node1 - 1] # Adjust for zero-based indexing + x2_coord, y2_coord = node_coordinates[:, node2 - 1] + x1_def = node_displacements[2 * (node1 - 1)] + y1_def = node_displacements[2 * (node1 - 1) + 1] + x2_def = node_displacements[2 * (node2 - 1)] + y2_def = node_displacements[2 * (node2 - 1) + 1] + ax.plot([x1_coord + x1_def, x2_coord + x2_def], [y1_coord + y1_def, y2_coord + y2_def], linestyle='-', linewidth=2.5, color=[0.4, 0.4, 0.4]) + + # Deformed nodes + for num_nod in range(number_nodes): + node_x_coord = node_coordinates[0, num_nod] + node_y_coord = node_coordinates[1, num_nod] + node_x_def = node_displacements[2 * num_nod] + node_y_def = node_displacements[2 * num_nod + 1] + ax.plot(node_x_coord + node_x_def, node_y_coord + node_y_def, + linestyle='none', linewidth=1, + marker='o', markersize=4, + markeredgecolor=[0.0, 0.0, 0.0], + markerfacecolor=[1.0, 1.0, 1.0]) + + # Deformed pin nodes + for num_pin in range(number_pin): + node = pin_nodes[num_pin] - 1 # Adjusting for zero-based indexing + pin_x_coord = node_coordinates[0, node] + pin_y_coord = node_coordinates[1, node] + pin_x_def = node_displacements[2 * node] + pin_y_def = node_displacements[2 * node + 1] + ax.plot(pin_x_coord + pin_x_def, pin_y_coord + pin_y_def, + linestyle='none', linewidth=1.5, + marker='o', markersize=5, + markeredgecolor=[0.0, 0.0, 0.0], markerfacecolor=[0.0, 0.0, 0.0]) + + + # Deformed roller nodes + for num_rol in range(number_roller): + node = roller_nodes[num_rol] - 1 # Adjusting for zero-based indexing + roller_x_coord = node_coordinates[0, node] + roller_y_coord = node_coordinates[1, node] + roller_x_def = node_displacements[2 * node] + roller_y_def = node_displacements[2 * node + 1] + ax.plot(roller_x_coord + roller_x_def, roller_y_coord + roller_y_def, + linestyle='none', linewidth=1.5, + marker='o', markersize=5, + markeredgecolor=[0.0, 0.0, 0.0], + markerfacecolor=[1.0, 1.0, 1.0]) + + + if save: + # Save figure as PDF + plt.savefig(file_path, format='pdf') + # NotImplementedError() + + + def plot_stress(self, info:Info, mesh:Mesh, forces:Forces, + displacements:Displacements, solution:Solution, + save:bool=True, show:bool=True): + # TODO some lists need to be converted to arrays. + project_dir = info.project_directory + file_name = info.file_name + paper_size = self.paper_size + paper_position = self.paper_position + # scale_factor = self.scale_factor + number_nodes = mesh.number_nodes + number_elements = mesh.number_elements + node_coordinates = np.transpose(np.array(mesh.node_coordinates)) + element_connectivity = np.array(mesh.element_connectivity) + number_pin = displacements.number_pin + pin_nodes = np.array(displacements.pin_nodes) + number_roller = displacements.number_roller + roller_nodes = np.array(displacements.roller_nodes) + roller_directions = displacements.roller_directions + roller_angles = displacements.roller_angles + element_stress = solution.element_stress + global_reactions = solution.global_reactions + + new_file_name = f"{file_name}_Stress.pdf" + file_path = f"{project_dir}/{new_file_name}" + + # File path creation + new_file_name = f"{file_name}_STRESS.pdf" + file_path = f"{project_dir}/{new_file_name}" + + # Figure and axes setup + fig, ax = plt.subplots() + fig.set_size_inches(self.paper_size[0] / 2.54, + self.paper_size[1] / 2.54) # Convert cm to inches + fig.patch.set_facecolor([1, 1, 1]) + ax.set_xlim(self.plot_x_limits) + ax.set_ylim(self.plot_y_limits) + ax.set_aspect('equal', adjustable='box') + ax.set_title(f"{file_name}: Stress") + ax.set_xlabel('x [m]') + ax.set_ylabel('y [m]') + + self._plot_rollers(node_coordinates.T, + displacements=displacements, + ax=ax) + + # Elements + pos_stress = element_stress[element_stress >= 0] + neg_stress = element_stress[element_stress <= 0] + max_pos_stress = max(pos_stress) + max_neg_stress = min(neg_stress) + for num_ele in range(number_elements): + node1, node2 = element_connectivity[num_ele, :] + x1_coord, y1_coord = node_coordinates[:, node1 - 1] # Adjust for zero-based indexing + x2_coord, y2_coord = node_coordinates[:, node2 - 1] + element_color = get_colors(element_stress[num_ele], max_pos_stress, max_neg_stress) + ax.plot([x1_coord, x2_coord], [y1_coord, y2_coord], + linestyle='-', linewidth=3, color=element_color) + + # Forces vectors + self._plot_force_vectors(node_coordinates.T, forces, ax) + + # Reaction vectors + s_node = 0 # Adjusted for zero-based indexing + # Pin reactions + for num_pin in range(number_pin): + p_node = pin_nodes[num_pin] - 1 + p_reaction = global_reactions[:, s_node] + # x component + x_react = p_reaction[0] + f_dir_vec = np.array([x_react, 0]) / abs(x_react) + draw_seg = get_force_arrow(node_coordinates[:, p_node], f_dir_vec, 8, self.plot_scale) + for seg in range(3): + ax.plot(draw_seg[2*seg:2*seg+2, 0], draw_seg[2*seg:2*seg+2, 1], + # draw_seg[2 * seg - 1, :], draw_seg[2 * seg, :], + linestyle='-', linewidth=1, color=[0.5, 0.5, 0.5]) + + # y component + y_react = p_reaction[1] + f_dir_vec = np.array([0, y_react]) / abs(y_react) + draw_seg = get_force_arrow(node_coordinates[:, p_node], f_dir_vec, 8, self.plot_scale) + for seg in range(3): + ax.plot(draw_seg[2*seg:2*seg+2, 0], draw_seg[2*seg:2*seg+2, 1], + # draw_seg[2 * seg - 1, :], draw_seg[2 * seg, :], + linestyle='-', linewidth=1, color=[0.5, 0.5, 0.5]) + s_node += 1 + + # Roller reactions + for num_rol in range(number_roller): + r_node = roller_nodes[num_rol] - 1 + r_reaction = global_reactions[:, s_node] + f_dir_vec = r_reaction / np.linalg.norm(r_reaction) + draw_seg = get_force_arrow(node_coordinates[:, r_node], f_dir_vec, 8, self.plot_scale) + for seg in range(3): + ax.plot(draw_seg[2*seg:2*seg+2, 0], draw_seg[2*seg:2*seg+2, 1], + # draw_seg[2 * seg - 1, :], draw_seg[2 * seg, :], + linestyle='-', linewidth=1, color=[0.5, 0.5, 0.5]) + s_node += 1 + + # plot nodes, Pin Nodes, and Roller Nodes + self._plot_all_nodes(mesh=mesh, displacements=displacements, ax=ax) + + # plot element numbers + self._plot_element_numbers(mesh, ax) + # Save and/or show plot + if save: + plt.savefig(file_path) + if show: + plt.show() + + + # NotImplementedError() +#%% + +if __name__ == '__main__': + # pp_project_dir = pathlib.Path('example-np') + pp_project_dir = pathlib.Path('../../examples/exam2024-01') + info = Info(project_directory=str(pp_project_dir.absolute()), file_name='test') + + fileData = FileData.from_directory(info.project_directory) + mesh = Mesh() + mesh.process_mesh(file_data= fileData.mesh) + + displacements = Displacements() + displacements.process_displacements(file_data= fileData.displacements) + + forces = Forces() + forces.process_forces(file_data= fileData.forces) + write_input_data(info=info, mesh=mesh, displacements=displacements, forces=forces) + + dofs = Dofs() + dofs.process_dofs(mesh=mesh, displacements=displacements) + analysis = Analysis() + analysis.get_global_stiffness_matrix(mesh=mesh) + analysis.get_global_force_vector(forces=forces, dofs=dofs) + analysis.get_new_displacement_vector(displacements=displacements, dofs=dofs) + analysis.get_new_transformation_matrix(displacements=displacements, dofs=dofs) + #================== Solution ================== + solution = Solution() + solution.solve_displacement(analysis, dofs) + solution.solve_reaction(displacements=displacements) + solution.solve_stress(mesh=mesh) + # Usage example + # Assume info, mesh, displacements, and solution are instances of their respective classes with attributes set + write_results(info, mesh=mesh, displacements=displacements, solution=solution) + + # %% + tp = TrussPlotter() + tp.get_plot_parameters(mesh=mesh, solution=solution) + + tp.plot_truss(info, mesh, forces, displacements, save=True, show=True) + + # %% + tp.plot_deformation(info, mesh, forces, displacements, solution, save=False, show=True) + # %% + tp.plot_stress(info, mesh, forces, displacements, solution, save=False, show=True) +# %% diff --git a/src/npp_2d_truss_analysis/truss_project.py b/src/npp_2d_truss_analysis/truss_project.py new file mode 100644 index 0000000..577a060 --- /dev/null +++ b/src/npp_2d_truss_analysis/truss_project.py @@ -0,0 +1,154 @@ +#%% +import pathlib +import numpy as np +import tkinter as tk +from tkinter import filedialog + +from npp_2d_truss_analysis.truss_input import Info, FileData,Mesh,Displacements,Forces, write_input_data +from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis +from npp_2d_truss_analysis.truss_solution import Solution, write_results +from npp_2d_truss_analysis.truss_plotter import TrussPlotter + +#%% +class TrussAnalysisProject: + _mesh:Mesh = None + _displacements:Displacements = None + _forces:Forces = None + _dofs:Dofs = None + _analysis:Analysis = None + _solution:Solution = None + _tp:TrussPlotter = TrussPlotter() + + def __init__(self,info:Info, mesh:Mesh, displacements:Displacements, forces:Forces): + self._info = info + self._mesh = mesh + self._displacements = displacements + self._forces = forces + + def update_matrices(self): + self._dofs = Dofs() + self._dofs.process_dofs(mesh=self._mesh, displacements=self._displacements) + self._analysis = Analysis() + self._analysis.get_global_stiffness_matrix(mesh=self._mesh) + self._analysis.get_global_force_vector(forces=self._forces, dofs=self._dofs) + self._analysis.get_new_displacement_vector(displacements=self._displacements, dofs=self._dofs) + self._analysis.get_new_transformation_matrix(displacements=self._displacements, dofs=self._dofs) + + def solve(self, to_disk:bool = True): + # force the update of the matrix analysis + self.update_matrices() + #================== Solution ================== + self._solution = Solution() + self._solution.solve_displacement(self._analysis, self._dofs) + self._solution.solve_reaction(displacements=self._displacements) + self._solution.solve_stress(mesh=self._mesh) + # Usage example + # Assume info, mesh, displacements, and solution are instances of their respective classes with attributes set + if to_disk: + write_results(self._info, mesh=self._mesh, displacements=self._displacements, solution=self._solution) + + def write_input_data(self): + write_input_data(info=self._info, mesh=self._mesh, displacements=self._displacements, forces=self._forces) + + + def plot_truss(self, save:bool=True, show:bool=True): + self._tp.get_plot_parameters(mesh=self._mesh, solution=None) + + # Usage example + # Assume info, plot, mesh, forces, and displacements are instances of their respective classes with attributes set + self._tp.plot_truss(self._info, self._mesh, self._forces, self._displacements, save=save, show=show) + + def plot_deformation(self, save:bool=True, show:bool=True): + # Requires solution to be calculated + self._tp.get_plot_parameters(mesh=self._mesh, solution=self._solution) + + # Usage example + # Assume info, plot, mesh, forces, and displacements are instances of their respective classes with attributes set + self._tp.plot_deformation(info=self._info, + mesh=self._mesh, forces=self._forces, + displacements=self._displacements, + solution=self._solution, + save=save, show=show) + + def plot_stresses(self, save:bool=True, show:bool=True): + # Requires solution to be calculated + self._tp.get_plot_parameters(mesh=self._mesh, solution=self._solution) + + # Usage example + # Assume info, plot, mesh, forces, and displacements are instances of their respective classes with attributes set + self._tp.plot_stress(info=self._info, + mesh=self._mesh, forces=self._forces, + displacements=self._displacements, + solution=self._solution, + save=save, show=show) + + + def report_reactions(self, fmt:str = '.3g'): + """prints the reaction forces on each support using a specified format""" + print('Reaction forces:') + print('|- Pinned Nodes:') + for i in range(self._displacements.number_pin): + f_x = self._solution.global_reactions[0,i] + f_y = self._solution.global_reactions[1,i] + print(f"| |- Node {self._displacements.pin_nodes[i]} = ({format(f_x, fmt)}, {format(f_y, fmt)}) | mag: {format(np.sqrt(f_x**2+f_y**2), fmt)}") + print('|- Roller Nodes:') + for i in range(self._displacements.number_roller): + j = i + self._displacements.number_pin + f_x = self._solution.global_reactions[0,j] + f_y = self._solution.global_reactions[1,j] + print(f"| |- Node {self._displacements.roller_nodes[i]} = ({format(f_x, fmt)}, {format(f_y, fmt)}) | mag: {format(np.sqrt(f_x**2+f_y**2), fmt)}") + + + def report_rod_forces(self, fmt:str = '.3g'): + """prints the forces on each member using a specified format""" + print('Rods forces:') + for i in range(self._mesh.number_elements): + f = self._solution.element_force[i] + print(f"| |- Rod {i+1} = {format(f, fmt)}") + + @classmethod + def from_json_file(cls, json_file_name:str, info:Info): + # read the JSON_FILE_NAME file content into json_problem_data string + if isinstance(json_file_name, pathlib.Path): + json_file_name = str(json_file_name.absolute()) + with open(json_file_name, 'r') as json_file: + json_problem_data = json_file.read() + + return cls.from_json(json_text=json_problem_data, info=info) + + @classmethod + def from_json(cls, json_text:str, info:Info=None): + + mesh = Mesh.from_json(json_data=json_text) + + displacements = Displacements.from_json(json_str=json_text) + # displacements.process_displacements(file_data= fileData.displacements) + + forces = Forces.from_json_str(json_text) + return cls(info=info, mesh=mesh, displacements=displacements, forces=forces) + +if __name__ == "__main__": + + pp_project_dir = pathlib.Path('exam2024-01') + pp_project_dir = pathlib.Path('../examples/example_101') + FNAME_PREFIX = 'test' + + + info = Info(project_directory=str(pp_project_dir.absolute()), file_name=FNAME_PREFIX) + + fileData = FileData.from_directory(info.project_directory) + mesh = Mesh() + mesh.process_mesh(file_data= fileData.mesh) + + displacements = Displacements() + displacements.process_displacements(file_data= fileData.displacements) + forces = Forces() + forces.process_forces(file_data= fileData.forces) + #%% + truss_problem = TrussAnalysisProject(info=info, mesh=mesh, displacements=displacements, forces=forces) + truss_problem.write_input_data() + truss_problem.solve() + truss_problem.plot_truss(save=True, show=True) +# %% + +# %% diff --git a/src/npp_2d_truss_analysis/truss_solution.py b/src/npp_2d_truss_analysis/truss_solution.py new file mode 100644 index 0000000..92fa7c5 --- /dev/null +++ b/src/npp_2d_truss_analysis/truss_solution.py @@ -0,0 +1,286 @@ +#%% +import pathlib +import numpy as np +import tkinter as tk +from tkinter import filedialog + +from npp_2d_truss_analysis.truss_input import Info, FileData,Mesh,Displacements,Forces, write_input_data +from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis + +class Solution: + def __init__(self): + self.new_displacements = None + self.new_forces = None + self.global_displacements = None + self.global_forces = None + self.global_reactions = None + self.element_stress = None + self.element_force = None + + def solve_displacement(self, analysis:Analysis, dofs:Dofs): + fixed_dofs = (np.array([dofs.fixed_dofs])-1).flatten() + free_dofs = (np.array([dofs.free_dofs])-1).flatten() + f = analysis.force_global_vector + uc = analysis.displacement_new_vector + tc = analysis.transformation_new_matrix + k = analysis.stiffness_global_matrix + + # New force vector + fc = tc @ f + # New stiffness matrix + kc = tc @ k @ tc.T + + # Free new displacements + # uc[free_dofs] = np.linalg.solve(kc[np.ix_(free_dofs, free_dofs)], + # fc[free_dofs] - kc[np.ix_(free_dofs, fixed_dofs)] @ uc[fixed_dofs]) + uc[free_dofs] = np.linalg.inv(kc[np.ix_(free_dofs, free_dofs)])@ fc[free_dofs] - kc[np.ix_(free_dofs, fixed_dofs)] @ uc[fixed_dofs] + + + # Fixed new forces + fc[fixed_dofs] = kc[np.ix_(fixed_dofs, free_dofs)] @ uc[free_dofs] + \ + kc[np.ix_(fixed_dofs, fixed_dofs)] @ uc[fixed_dofs] - fc[fixed_dofs] + + # Global displacement and force vectors + u = tc.T @ uc + f = tc.T @ fc + + self.new_displacements = uc + self.new_forces = fc + self.global_displacements = u + self.global_forces = f + + + def solve_reaction(self, displacements:Displacements): + """returns the global reactions in a 2x(number of supports) array + + row 1 : x component + row 2 : y component + + Starts with pin + + Args: + displacements(Displacements): Container with the displacements data + returns + + self.global_reactions (ndarray[2x(number of supports)]): global reactions. + """ + fc = self.new_forces + number_pin = displacements.number_pin + pin_nodes = displacements.pin_nodes + number_roller = displacements.number_roller + roller_nodes = displacements.roller_nodes + roller_directions = displacements.roller_directions + roller_angles = displacements.roller_angles + number_support = displacements.number_support + + r = np.zeros((2, number_support)) + s_node = 0 + + # Pin support + for i in range(number_pin): + p_node = pin_nodes[i] + fixed_dofs = [2 * (p_node-1) , 2 * (p_node-1)+1] + r[:, s_node] = fc[fixed_dofs] + # print(f"{r[:, s_node] }") + s_node += 1 + + # Roller support + for i in range(number_roller): + r_node = roller_nodes[i]-1 + r_direction = roller_directions[i] + r_angle = roller_angles[i] + rc = np.zeros(2) + if r_direction == 1: + # fixes the normal direction, allows rolling on x + fixed_dof = 2 * r_node+1 + rc[1] = fc[fixed_dof] + else: + # r_direction ==2 fixes the horizontal direction + fixed_dof = 2 * r_node + rc[0] = fc[fixed_dof] + + c = np.cos(np.radians(r_angle)) + s = np.sin(np.radians(r_angle)) + rot = np.array([[c, s], [-s, c]]) + r[:, s_node] = rot.T @ rc + # logging.debug(f"{r[:, s_node] }") + s_node += 1 + + self.global_reactions = r + return self.global_reactions + + def solve_stress(self, mesh:Mesh): + u = self.global_displacements + number_elements = mesh.number_elements + node_coordinates = mesh.node_coordinates + element_connectivity = mesh.element_connectivity + material_e = mesh.young_modulus + material_a = mesh.area + + element_stress = np.zeros(number_elements) + element_force = np.zeros(number_elements) + + for i in range(number_elements): + # Element nodes + node1, node2 = element_connectivity[i] + # Element DOFs + element_dofs = [2 * (node1 - 1), 2 * (node1-1)+1, 2 * (node2 - 1), 2 *( node2-1)+1] + # Element material constants + e = material_e[i] + a = material_a[i] + # Element components and length + dx = node_coordinates[node2-1][0] - node_coordinates[node1-1][0] + dy = node_coordinates[node2-1][1] - node_coordinates[node1-1][1] + l = np.sqrt(dx**2 + dy**2) + # Sine and cosine of angle between reference frames + c = dx / l + s = dy / l + # Element transformation matrix + t = np.array([[c, s, 0, 0], [-s, c, 0, 0], [0, 0, c, s], [0, 0, -s, c]]) + # Local element displacements + ul = t @ u[element_dofs] + # Element stress + element_stress[i] = e * 1 / l * np.array([-1, 0, 1, 0]) @ ul + # Element force + element_force[i] = element_stress[i] * a + + self.element_stress = element_stress + self.element_force = element_force + + + def get_max_displacement(self)->float: + """Returns the maximum displacement + + convenience method to obtain the maximum displacement for the paper size. + + Returns: + float: maximum displacement or 0 if no displacements are available + """ + try: + u = self.global_displacements + return np.max(np.abs(u)) + except (TypeError, AttributeError): + return 0 + + def report_displacements(self, mesh:Mesh)->str: + """Returns a string with the displacements report + + #TODO Do the same for reactions and stresses + + Returns: + str: displacements report + """ + number_nodes = mesh.number_nodes + u = self.global_displacements + r = self.global_reactions + + bar_line = '-' * 40 + ret_str=' NODE DISPLACEMENTS\n' + ret_str +=(f'{bar_line}\n') + ret_str +=('NODE DX(M) DY(M) DM(M)\n') + for i in range(number_nodes): + displ_x = u[2*i] + displ_y = u[2*i + 1] + displ_m = (displ_x**2 + displ_y**2)**0.5 + ret_str+=(f'{i+1:<4} {displ_x:11.3E} {displ_y:11.3E} {displ_m:11.3E}\n') + return ret_str + +def write_results(info, mesh:Mesh, displacements:Displacements, solution:Solution): + project_dir = info.project_directory + file_name = info.file_name + number_nodes = mesh.number_nodes + number_elements = mesh.number_elements + number_support = displacements.number_support + support_nodes = displacements.support_nodes + u = solution.global_displacements + r = solution.global_reactions + element_stress = solution.element_stress + element_force = solution.element_force + + new_file_name = f"{file_name}_RESULTS.dat" + file_path = f"{project_dir}/{new_file_name}" + + with open(file_path, 'w') as file: + bar_line = '-' * 40 + + # Writing node displacements + file.write(' NODE DISPLACEMENTS\n') + file.write(f'{bar_line}\n') + file.write('NODE DX(M) DY(M) DM(M)\n') + for i in range(number_nodes): + displ_x = u[2*i] + displ_y = u[2*i + 1] + displ_m = (displ_x**2 + displ_y**2)**0.5 + file.write(f'{i+1:<4} {displ_x:11.3E} {displ_y:11.3E} {displ_m:11.3E}\n') + file.write('\n') + + # Writing element forces and stresses + file.write(' ELEMENT FORCES AND STRESSES\n') + file.write(f'{bar_line}\n') + file.write('EL. FORCE(N) STRESS(PA)\n') + for i in range(number_elements): + elem_force = element_force[i] + elem_stress = element_stress[i] + file.write(f'{i+1:<3} {elem_force:+14.6E} {elem_stress:+14.6E}\n') + file.write('\n') + + # Writing support reactions + file.write(' SUPPORT REACTIONS\n') + file.write(f'{bar_line}\n') + file.write('NODE RX(N) RY(N) RM(N)\n') + for i in range(number_support): + s_node = support_nodes[i] + react_x = r[0, i] + react_y = r[1, i] + react_m = (react_x**2 + react_y**2)**0.5 + file.write(f'{s_node:<4} {react_x:11.3E} {react_y:11.3E} {react_m:11.3E}\n') + + +#%% + +if __name__ == '__main__': + pp_project_dir = pathlib.Path('example-np') + info = Info(project_directory=str(pp_project_dir.absolute()), file_name='test') + + fileData = FileData.from_directory(info.project_directory) + mesh = Mesh() + mesh.process_mesh(file_data= fileData.mesh) + + displacements = Displacements() + displacements.process_displacements(file_data= fileData.displacements) + + forces = Forces() + forces.process_forces(file_data= fileData.forces) + write_input_data(info=info, mesh=mesh, displacements=displacements, forces=forces) + + dofs = Dofs() + dofs.process_dofs(mesh=mesh, displacements=displacements) + analysis = Analysis() + analysis.get_global_stiffness_matrix(mesh=mesh) + analysis.get_global_force_vector(forces=forces, dofs=dofs) + analysis.get_new_displacement_vector(displacements=displacements, dofs=dofs) + analysis.get_new_transformation_matrix(displacements=displacements, dofs=dofs) + #%% + solution = Solution() + # Assume analysis and dofs are instances of their respective classes with attributes set + solution.solve_displacement(analysis, dofs) + print("Displacements==============================") + print(solution.new_displacements) + print(solution.new_displacements.shape) + print(solution.new_displacements.dtype) + print(f"Global displacements: {solution.global_displacements}") + print(f"New forces: {solution.new_forces}") + print(f"Global forces: {solution.global_forces}") + + # %% + print("Solve Reactions==================================") + solution.solve_reaction(displacements=displacements) + print(solution.global_reactions) + print(solution.global_reactions.shape) + print(solution.global_reactions.dtype) + + # %% + solution.solve_stress(mesh=mesh) + # Usage example + # Assume info, mesh, displacements, and solution are instances of their respective classes with attributes set + write_results(info, mesh=mesh, displacements=displacements, solution=solution) diff --git a/src/setup.py b/src/setup.py new file mode 100644 index 0000000..e49af2b --- /dev/null +++ b/src/setup.py @@ -0,0 +1,69 @@ +import setuptools + + +def get_version(rel_path): + """Get the version string from a file. + + Assuming the version line is in the form: __version__ = '0.1.0' + strips out the version and remove leading and trailing whitespace and quotes + + Args: + rel_path (str): The relative path to the file. + + Raises: + RuntimeError: If the version string is not found. + + Returns: + str: The version string. + """ + with open(rel_path, 'r', encoding='utf-8') as fp: + for line in fp: + if line.startswith('__version__'): + delim = '"' if '"' in line else "'" + return line.split(delim)[1] + raise RuntimeError("Unable to find version string.") + +__version__ = get_version('npp_2d_truss_analysis/__init__.py') + +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +requirements = [ + 'matplotlib', + 'scipy', + 'numpy', +] + +test_requirements = [ + 'pytest', + # 'pytest-pep8', + # 'pytest-cov', +] + + +setuptools.setup( + name="npp_2d_truss_analysis", # Replace with your own username + version= __version__, + author="N. Papadakis", + author_email="npapnet@gmail.com", + description="A package for truss analysis with inclined roller support", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/npapnet/TrussAnalysis2D", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + install_requires=requirements, + tests_require=test_requirements, + python_requires='>=3.10', + project_urls={ + 'Source': 'https://github.com/npapnet/TrussAnalysis2D', + 'Documentation': 'https://npapnet.github.io/TrussAnalysis2D/', # TODO Update with your GitHub Pages URL + # 'Documentation': 'https://TrussAnalysis2D.github.io/en/latest/', + 'PyPI': 'https://pypi.org/project/npp_2d_truss_analysis/' + + }, +) \ No newline at end of file diff --git a/src/tests/test_forces_input.py b/src/tests/test_forces_input.py new file mode 100644 index 0000000..cd5f324 --- /dev/null +++ b/src/tests/test_forces_input.py @@ -0,0 +1,73 @@ +import pytest +from npp_2d_truss_analysis.truss_input import Mesh, Displacements, Forces + +@pytest.fixture +def forces_json_data(): + return """{ + "forces": [ + {"id": 1, "node":5, "direction": -180,"x": 40000, "y":0}, + {"id": 2, "node":3, "direction": 200,"x": 20000, "y":0} + ] +}""" + +def test_forces_json(forces_json_data): + forces = Forces() + forces.process_json(forces_json_data) + assert forces.number_forces == 2 + assert forces.force_nodes == [5, 3] + assert forces.force_components == [(40000.0, 0.0), (20000.0, 0.0)] + assert forces.force_angles == [-180.0, 200.0] + + +def test_forces_from_json(forces_json_data): + forces = Forces.from_json_str(forces_json_data) + assert forces.number_forces == 2 + assert forces.force_nodes == [5, 3] + assert forces.force_components == [(40000.0, 0.0), (20000.0, 0.0)] + assert forces.force_angles == [-180.0, 200.0] + + +def test_forces_update_force_node(forces_json_data): + forces = Forces.from_json_str(forces_json_data) + + f0bef = forces.get_force_by_id(id=0) + assert 0 == f0bef['id'] + assert 5 == f0bef['node'] + assert (40000.0, 0.0) == f0bef['fxy'] + assert -180 == f0bef['angle'] + # change node + forces.update_force_by_id(force_id=0, node=4) + f0aft = forces.get_force_by_id(id=0) + assert 0 == f0aft['id'] + assert 4 == f0aft['node'] + assert (40000.0, 0.0) == f0aft['fxy'] + assert -180 == f0aft['angle'] + # change force componetns + forces.update_force_by_id(force_id=0, fxy=(0, 10000)) + f0aft = forces.get_force_by_id(id=0) + assert 0 == f0aft['id'] + assert 4 == f0aft['node'] + assert (0, 10000) == f0aft['fxy'] + assert -180 == f0aft['angle'] + # change angle componets + forces.update_force_by_id(force_id=0, angle=160) + f0aft = forces.get_force_by_id(id=0) + assert 0 == f0aft['id'] + assert 4 == f0aft['node'] + assert (0, 10000) == f0aft['fxy'] + assert 160 == f0aft['angle'] + +def test_force_invalid_updates(forces_json_data): + forces = Forces.from_json_str(forces_json_data) + # invalid ID node + with pytest.raises(ValueError): + forces.update_force_by_id(force_id=-1, node=4) + with pytest.raises(ValueError): + forces.update_force_by_id(force_id=5, node=4) + # invalid fxy + with pytest.raises(AssertionError): + forces.update_force_by_id(force_id=0, fxy='a') + with pytest.raises(AssertionError): + forces.update_force_by_id(force_id=0, fxy=('1',0)) + with pytest.raises(AssertionError): + forces.update_force_by_id(force_id=0, fxy=(0,1,0)) \ No newline at end of file diff --git a/src/tests/test_input.py b/src/tests/test_input.py new file mode 100644 index 0000000..0949b16 --- /dev/null +++ b/src/tests/test_input.py @@ -0,0 +1,65 @@ +import pytest +from npp_2d_truss_analysis.truss_input import Mesh, Displacements, Forces + +@pytest.fixture +def mesh_data(): + return [[5.0], + [0.0, 0.0], + [0.0, 2.0], + [0.0, 4.0], + [4.0, 4.0], + [4.0, 2.0], + [7.0], + [4.0, 3.0, 1.0], + [3.0, 2.0, 1.0], + [2.0, 4.0, 1.0], + [4.0, 5.0, 1.0], + [5.0, 2.0, 1.0], + [2.0, 1.0, 1.0], + [1.0, 5.0, 1.0], + [1.0], + [200000000000.0, 1.0]] + +@pytest.fixture +def displ_data(): + return [[1.0], [4.0, 0.0, 0.0, 0.0], [1.0], [1.0, 2.0, 26.5651, 0.0], []] + +@pytest.fixture +def forces_data(): + return [[2.0], [5.0, -180.0, 40000.0, 0.0], [3.0, 200.0, 20000.0, 0.0]] + + +def test_mesh(mesh_data): + mesh = Mesh() + mesh.process_mesh(mesh_data) + assert mesh.number_nodes == 5 + assert mesh.number_elements == 7 + assert mesh.node_coordinates == [(0.0, 0.0), (0.0, 2.0), (0.0, 4.0), (4.0, 4.0), (4.0, 2.0)] + assert mesh.element_connectivity == [(4, 3), (3, 2), (2, 4), (4, 5), (5, 2), (2, 1), (1, 5)] + assert mesh.young_modulus == [200e9, 200e9, 200e9, 200e9, 200e9, 200e9, 200e9] + assert mesh.area == [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] + +def test_displ(displ_data): + displ = Displacements() + displ.process_displacements(displ_data) + assert displ.number_pin == 1 + assert displ.pin_nodes == [4] + assert displ.pin_displacements == [(0.0, 0.0)] + assert displ.pin_angles == [0.0] + assert displ.number_roller == 1 + assert displ.roller_nodes == [1] + assert displ.roller_directions == [2] + assert displ.roller_angles == [26.5651] + assert displ.roller_displacements == [0.0] + assert displ.number_support == 2 + assert displ.support_nodes == [4,1] + + + +def test_forces(forces_data): + forces = Forces() + forces.process_forces(forces_data) + assert forces.number_forces == 2 + assert forces.force_nodes == [5, 3] + assert forces.force_components == [(40000.0, 0.0), (20000.0, 0.0)] + assert forces.force_angles == [-180.0, 200.0] \ No newline at end of file diff --git a/src/tests/test_input_json.py b/src/tests/test_input_json.py new file mode 100644 index 0000000..0ce6e18 --- /dev/null +++ b/src/tests/test_input_json.py @@ -0,0 +1,189 @@ +import pytest +from npp_2d_truss_analysis.truss_input import Mesh, Displacements, Forces, Info +from npp_2d_truss_analysis.truss_project import TrussAnalysisProject + +@pytest.fixture +def mesh_json_data(): + return """{ + "nodes": [ + {"id": 1, "coordinates": [0, 0]}, + {"id": 2, "coordinates": [0, 2]}, + {"id": 3, "coordinates": [0, 4]}, + {"id": 4, "coordinates": [4, 4]}, + {"id": 5, "coordinates": [4, 2]} + ], + "elements": [ + {"id": 1, "connectivity": [4, 3], "materialId": 1}, + {"id": 2, "connectivity": [3, 2], "materialId": 1}, + {"id": 3, "connectivity": [2, 4], "materialId": 1}, + {"id": 4, "connectivity": [4, 5], "materialId": 1}, + {"id": 5, "connectivity": [5, 2], "materialId": 1}, + {"id": 6, "connectivity": [2, 1], "materialId": 1}, + {"id": 7, "connectivity": [1, 5], "materialId": 1} + ], + "materials": [ + {"id": 1, "youngModulus": 200000000000.0, "area": 1.0} + ] + }""" + +@pytest.fixture +def displ_json_data(): + return """{ + "pin": [ + {"id": 1, "node":4, "angle": 0,"dx": 0, "dy":0} + ], + "rollers": [ + {"id": 1, "node": 1, "direction": 2, "angle": 26.5651, "dx":0} + ] + }""" + + +@pytest.fixture +def forces_json_data(): + return """{ + "forces": [ + {"id": 1, "node":5, "direction": -180,"x": 40000, "y":0}, + {"id": 2, "node":3, "direction": 200,"x": 20000, "y":0} + ] +}""" + + +def test_mesh_json(mesh_json_data): + mesh = Mesh() + mesh.process_mesh_json(mesh_json_data) + assert mesh.number_nodes == 5 + assert mesh.number_elements == 7 + assert mesh.node_coordinates == [(0.0, 0.0), (0.0, 2.0), (0.0, 4.0), (4.0, 4.0), (4.0, 2.0)] + assert mesh.element_connectivity == [(4, 3), (3, 2), (2, 4), (4, 5), (5, 2), (2, 1), (1, 5)] + assert mesh.young_modulus == [200e9, 200e9, 200e9, 200e9, 200e9, 200e9, 200e9] + assert mesh.area == [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] + +def test_displ_json(displ_json_data): + displ = Displacements() + displ.process_json(displ_json_data) + assert displ.number_pin == 1 + assert displ.pin_nodes == [4] + assert displ.pin_displacements == [(0.0, 0.0)] + assert displ.pin_angles == [0.0] + assert displ.number_roller == 1 + assert displ.roller_nodes == [1] + assert displ.roller_directions == [2] + assert displ.roller_angles == [26.5651] + assert displ.roller_displacements == [0.0] + assert displ.number_support == 2 + assert displ.support_nodes == [4,1] + +def test_forces_json(forces_json_data): + forces = Forces() + forces.process_json(forces_json_data) + assert forces.number_forces == 2 + assert forces.force_nodes == [5, 3] + assert forces.force_components == [(40000.0, 0.0), (20000.0, 0.0)] + assert forces.force_angles == [-180.0, 200.0] + + +@pytest.fixture +def problem_json_data(): + return """{ + "mesh":{ + "nodes": [ + {"id": 1, "coordinates": [0, 0]}, + {"id": 2, "coordinates": [0, 2]}, + {"id": 3, "coordinates": [0, 4]}, + {"id": 4, "coordinates": [4, 4]}, + {"id": 5, "coordinates": [4, 2]} + ], + "elements": [ + {"id": 1, "connectivity": [4, 3], "materialId": 1}, + {"id": 2, "connectivity": [3, 2], "materialId": 1}, + {"id": 3, "connectivity": [2, 4], "materialId": 1}, + {"id": 4, "connectivity": [4, 5], "materialId": 1}, + {"id": 5, "connectivity": [5, 2], "materialId": 1}, + {"id": 6, "connectivity": [2, 1], "materialId": 1}, + {"id": 7, "connectivity": [1, 5], "materialId": 1} + ], + "materials": [ + {"id": 1, "youngModulus": 200000000000.0, "area": 1.0} + ] + }, + "displacements":{ + "pin": [ + {"id": 1, "node":4, "angle": 0,"dx": 0, "dy":0} + ], + "rollers": [ + {"id": 1, "node": 1, "direction": 1, "angle": -63.4349, "dx":0} + ] + }, + "forces": [ + {"id": 1, "node":5, "direction": -180,"x": 40000, "y":0}, + {"id": 2, "node":3, "direction": 200,"x": 20000, "y":0} + ] +}""" + + +def test_problem_json(problem_json_data): + + mesh = Mesh() + mesh.process_mesh_json(problem_json_data) + assert mesh.number_nodes == 5 + assert mesh.number_elements == 7 + assert mesh.node_coordinates == [(0.0, 0.0), (0.0, 2.0), (0.0, 4.0), (4.0, 4.0), (4.0, 2.0)] + assert mesh.element_connectivity == [(4, 3), (3, 2), (2, 4), (4, 5), (5, 2), (2, 1), (1, 5)] + assert mesh.young_modulus == [200e9, 200e9, 200e9, 200e9, 200e9, 200e9, 200e9] + assert mesh.area == [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] + + displ = Displacements() + displ.process_json(problem_json_data) + assert displ.number_pin == 1 + assert displ.pin_nodes == [4] + assert displ.pin_displacements == [(0.0, 0.0)] + assert displ.pin_angles == [0.0] + assert displ.number_roller == 1 + assert displ.roller_nodes == [1] + assert displ.roller_directions == [1] + assert displ.roller_angles == [-63.4349] + assert displ.roller_displacements == [0.0] + assert displ.number_support == 2 + assert displ.support_nodes == [4,1] + + + forces = Forces() + forces.process_json(problem_json_data) + assert forces.number_forces == 2 + assert forces.force_nodes == [5, 3] + assert forces.force_components == [(40000.0, 0.0), (20000.0, 0.0)] + assert forces.force_angles == [-180.0, 200.0] + + + +def test_problem_single_json(problem_json_data): + + info = Info(project_directory='./', file_name='test') + tp = TrussAnalysisProject.from_json(info = info, json_text=problem_json_data) + mesh = tp._mesh + assert mesh.number_nodes == 5 + assert mesh.number_elements == 7 + assert mesh.node_coordinates == [(0.0, 0.0), (0.0, 2.0), (0.0, 4.0), (4.0, 4.0), (4.0, 2.0)] + assert mesh.element_connectivity == [(4, 3), (3, 2), (2, 4), (4, 5), (5, 2), (2, 1), (1, 5)] + assert mesh.young_modulus == [200e9, 200e9, 200e9, 200e9, 200e9, 200e9, 200e9] + assert mesh.area == [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] + + displ = tp._displacements + assert displ.number_pin == 1 + assert displ.pin_nodes == [4] + assert displ.pin_displacements == [(0.0, 0.0)] + assert displ.pin_angles == [0.0] + assert displ.number_roller == 1 + assert displ.roller_nodes == [1] + assert displ.roller_directions == [1] + assert displ.roller_angles == [-63.4349] + assert displ.roller_displacements == [0.0] + assert displ.number_support == 2 + assert displ.support_nodes == [4,1] + + + forces = tp._forces + assert forces.number_forces == 2 + assert forces.force_nodes == [5, 3] + assert forces.force_components == [(40000.0, 0.0), (20000.0, 0.0)] + assert forces.force_angles == [-180.0, 200.0] \ No newline at end of file diff --git a/src/tests/test_mesh.py b/src/tests/test_mesh.py new file mode 100644 index 0000000..cd4fb6a --- /dev/null +++ b/src/tests/test_mesh.py @@ -0,0 +1,140 @@ +import pytest +import numpy as np +from npp_2d_truss_analysis.truss_input import Mesh, Displacements, Forces +from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis +from npp_2d_truss_analysis.truss_solution import Solution + + +@pytest.fixture +def mesh_inst(): + mesh_data = [[5.0], + [0.0, 0.0], + [0.0, 2.0], + [0.0, 4.0], + [4.0, 4.0], + [4.0, 2.0], + [7.0], + [4.0, 3.0, 1.0], + [3.0, 2.0, 1.0], + [2.0, 4.0, 1.0], + [4.0, 5.0, 1.0], + [5.0, 2.0, 1.0], + [2.0, 1.0, 1.0], + [1.0, 5.0, 1.0], + [1.0], + [200000000000.0, 1.0]] + _mesh = Mesh() + _mesh.process_mesh(mesh_data) + return _mesh + + +@pytest.fixture +def displ(): + displ_data = [[1.0], [4.0, 0.0, 0.0, 0.0], [1.0], [1.0, 2.0, 26.5651, 0.0], []] + _displ = Displacements() + _displ.process_displacements(displ_data) + return _displ + +@pytest.fixture +def forces(): + forces_data = [[2.0], [5.0, -180.0, 40000.0, 0.0], [3.0, 200.0, 20000.0, 0.0]] + _forces = Forces() + _forces.process_forces(forces_data) + return _forces + +def test_dofs(mesh_inst, displ, forces): + dofs = Dofs() + dofs.process_dofs(mesh=mesh_inst, displacements=displ) + assert dofs.number_dofs == 10 + assert dofs.number_fixed == 3 + assert dofs.number_free == 7 + assert dofs.fixed_dofs == [7, 8, 1] + assert dofs.free_dofs == [2, 3, 4, 5, 6, 9, 10] + + +def test_tm(mesh_inst, displ, forces): + """Transformation matrix test + + """ + dofs = Dofs() + dofs.process_dofs(mesh=mesh_inst, displacements=displ) + + analysis = Analysis() + # Assume mesh is an instance of the Mesh class with attributes set + analysis.get_global_stiffness_matrix(mesh=mesh_inst) + analysis.get_global_force_vector(forces=forces, dofs=dofs) + analysis.get_new_displacement_vector(displacements=displ, dofs=dofs) + analysis.get_new_transformation_matrix(displacements=displ, dofs=dofs) + + expected = np.eye(10) + expected[0,0] = 0.89442681 + expected[0,1] = 0.44721436 + expected[1,0] = -0.44721436 + expected[1,1] = 0.89442681 + + np.testing.assert_allclose(expected, analysis.transformation_new_matrix, rtol=1e-5, atol=1e-5) + + +def test_solution_solve_displacements(mesh_inst, displ, forces): + """Transformation matrix test + + """ + dofs = Dofs() + dofs.process_dofs(mesh=mesh_inst, displacements=displ) + + analysis = Analysis() + # Assume mesh is an instance of the Mesh class with attributes set + analysis.get_global_stiffness_matrix(mesh=mesh_inst) + analysis.get_global_force_vector(forces=forces, dofs=dofs) + analysis.get_new_displacement_vector(displacements=displ, dofs=dofs) + analysis.get_new_transformation_matrix(displacements=displ, dofs=dofs) + solution = Solution() + # Assume analysis and dofs are instances of their respective classes with attributes set + solution.solve_displacement(analysis, dofs) + + expected = [0, 3.2521e-07, -5.2783e-07, 2.9088e-07, -3.7588e-07, 2.2248e-07, + 0, 0, -8.0144e-07, 1.3160e-07] + + np.testing.assert_allclose(expected, solution.new_displacements, rtol=1e-5, atol=1e-5) + expected_new_forces = [ 2.9426e+04, 0, 0, 0, -1.8794e+04, -6.8404e+03, + 3.2475e+04, -6.3193e+03, -4.0000e+04, -4.8984e-12] + np.testing.assert_allclose(expected_new_forces, solution.new_forces, rtol=1e-4) + + expected_gl_displ = [ -1.4544e-07,2.9088e-07, -5.2783e-07, 2.9088e-07, + -3.7588e-07, 2.2248e-07, 0, 0, -8.0144e-07, 1.3160e-07] + np.testing.assert_allclose(expected_gl_displ, solution.global_displacements, rtol=1e-4) + + expected_gl_forces = [2.6319e+04, 1.3160e+04, 0, 0, -1.8794e+04, -6.8404e+03, 3.2475e+04, -6.3193e+03, -4.0000e+04, -4.8984e-12] + np.testing.assert_allclose(expected_gl_forces, solution.global_forces, rtol=1e-4) + + + + +def test_solution_solve_reactions(mesh_inst, displ, forces): + """Transformation matrix test + + """ + dofs = Dofs() + dofs.process_dofs(mesh=mesh_inst, displacements=displ) + + analysis = Analysis() + # Assume mesh is an instance of the Mesh class with attributes set + analysis.get_global_stiffness_matrix(mesh=mesh_inst) + analysis.get_global_force_vector(forces=forces, dofs=dofs) + analysis.get_new_displacement_vector(displacements=displ, dofs=dofs) + analysis.get_new_transformation_matrix(displacements=displ, dofs=dofs) + solution = Solution() + # Assume analysis and dofs are instances of their respective classes with attributes set + solution.solve_displacement(analysis, dofs) + solution.solve_reaction( displacements= displ) + solution.solve_stress(mesh = mesh_inst) + + expected_gl_reactions = [ [3.2475e+04 , 2.6319e+04], + [-6.3193e+03, 1.3160e+04]] + np.testing.assert_allclose(expected_gl_reactions, solution.global_reactions, rtol=1e-4) + + expected_el_stress = [ 1.8794e+04, -6.8404e+03, 1.5296e+04, -1.3160e+04, -1.3681e+04, -2.8034e-02, -2.9426e+04] + np.testing.assert_allclose(expected_el_stress, solution.element_stress, rtol=1e-4) + + expected_el_forces = [ 1.8794e+04, -6.8404e+03, 1.5296e+04, -1.3160e+04, -1.3681e+04, -2.8034e-02, -2.9426e+04] + np.testing.assert_allclose(expected_el_forces, solution.element_force, rtol=1e-4) diff --git a/src/tests/test_truss_plotter.py b/src/tests/test_truss_plotter.py new file mode 100644 index 0000000..f9fa419 --- /dev/null +++ b/src/tests/test_truss_plotter.py @@ -0,0 +1,85 @@ +import pytest +import numpy as np +from npp_2d_truss_analysis.truss_input import Mesh, Displacements, Forces +from npp_2d_truss_analysis.truss_analysis_2d import Dofs, Analysis +from npp_2d_truss_analysis.truss_solution import Solution +from npp_2d_truss_analysis.truss_plotter import TrussPlotter + + +@pytest.fixture +def mesh_inst(): + mesh_data = [[5.0], + [0.0, 0.0], + [0.0, 2.0], + [0.0, 4.0], + [4.0, 4.0], + [4.0, 2.0], + [7.0], + [4.0, 3.0, 1.0], + [3.0, 2.0, 1.0], + [2.0, 4.0, 1.0], + [4.0, 5.0, 1.0], + [5.0, 2.0, 1.0], + [2.0, 1.0, 1.0], + [1.0, 5.0, 1.0], + [1.0], + [200000000000.0, 1.0]] + _mesh = Mesh() + _mesh.process_mesh(mesh_data) + return _mesh + + +@pytest.fixture +def displ(): + displ_data = [[1.0], [4.0, 0.0, 0.0, 0.0], [1.0], [1.0, 2.0, 26.5651, 0.0], []] + _displ = Displacements() + _displ.process_displacements(displ_data) + return _displ + +@pytest.fixture +def forces(): + forces_data = [[2.0], [5.0, -180.0, 40000.0, 0.0], [3.0, 200.0, 20000.0, 0.0]] + _forces = Forces() + _forces.process_forces(forces_data) + return _forces + +def test_dofs(mesh_inst, displ, forces): + dofs = Dofs() + dofs.process_dofs(mesh=mesh_inst, displacements=displ) + assert dofs.number_dofs == 10 + assert dofs.number_fixed == 3 + assert dofs.number_free == 7 + assert dofs.fixed_dofs == [7, 8, 1] + assert dofs.free_dofs == [2, 3, 4, 5, 6, 9, 10] + + + +def test_truss_plotter_config(mesh_inst, displ, forces): + """Transformation matrix test + + """ + dofs = Dofs() + dofs.process_dofs(mesh=mesh_inst, displacements=displ) + + analysis = Analysis() + analysis.get_global_stiffness_matrix(mesh=mesh_inst) + analysis.get_global_force_vector(forces=forces, dofs=dofs) + analysis.get_new_displacement_vector(displacements=displ, dofs=dofs) + analysis.get_new_transformation_matrix(displacements=displ, dofs=dofs) + solution = Solution() + # Assume analysis and dofs are instances of their respective classes with attributes set + solution.solve_displacement(analysis, dofs) + solution.solve_reaction( displacements= displ) + solution.solve_stress(mesh = mesh_inst) + + tp = TrussPlotter() + tp.get_plot_parameters(mesh=mesh_inst,solution=solution) + exp_scale_factor = 2.2459e+05 + assert tp.scale_factor == pytest.approx(exp_scale_factor, rel=1e-4) + + np.testing.assert_allclose(tp.paper_position, [ 0, 0, 29.7000, 21.0000]) + np.testing.assert_allclose(tp.paper_size, [ 29.7000, 21.0000]) + + assert tp.plot_scale == pytest.approx(3.9153, rel=1e-4) + np.testing.assert_allclose(tp.plot_x_limits, [-0.3600, 4.3600]) + np.testing.assert_allclose(tp.plot_y_limits, [-0.3600, 4.3600]) \ No newline at end of file diff --git a/trussanalysis2d/Files/Analysis/solveReaction.m b/trussanalysis2d/Files/Analysis/solveReaction.m index a913d8c..90a0971 100644 --- a/trussanalysis2d/Files/Analysis/solveReaction.m +++ b/trussanalysis2d/Files/Analysis/solveReaction.m @@ -28,8 +28,11 @@ rAngle = rollerAngles(numRol); Rc = zeros(2, 1); if (rDirection == 1) + % fixes the y direction + % assumes that the roller can roll in th x direction fixedDOF = 2 * rNode; Rc(2) = Fc(fixedDOF); + else fixedDOF = 2 * rNode - 1; Rc(1) = Fc(fixedDOF); diff --git a/trussanalysis2d/TrussAnalysis2D.m b/trussanalysis2d/TrussAnalysis2D.m index 73cee24..5aa591a 100644 --- a/trussanalysis2d/TrussAnalysis2D.m +++ b/trussanalysis2d/TrussAnalysis2D.m @@ -6,13 +6,13 @@ % Process user data -initializeVariables(); +initializeVariables(); [INFO] = getProjectInfo(INFO); [FILEDATA] = readPropertiesFiles(FILEDATA, INFO); -[MESH] = processMesh(FILEDATA, MESH); +[MESH] = processMesh(FILEDATA, MESH); [DISPLACEMENTS] = processDisplacements(FILEDATA, DISPLACEMENTS); [FORCES] = processForces(FILEDATA, FORCES);