├── tests ├── __init__.py ├── test_member_index.py ├── test_material_sources.py ├── test_custom_materials_and_shapes.py ├── example.trs ├── test_construction_and_io.py └── test_optimization.py ├── requirements.txt ├── .gitignore ├── .github └── workflows │ └── tests.yml ├── .readthedocs.yaml ├── setup.py ├── LICENSE ├── README.md ├── trussme ├── __init__.py ├── visualize.py ├── report.py ├── optimize.py ├── components.py └── truss.py └── examples └── optimizing_for_mass.ipynb /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | pandas 3 | tabulate 4 | matplotlib 5 | scipy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | tests/report* 3 | tests/asdf* 4 | tests/optim* 5 | tests/__pychache__/* 6 | trussme/__pycache__/* 7 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | tests: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.x' 22 | - name: install dependencies 23 | run: pip install -r requirements.txt pytest 24 | - name: run tests 25 | run: pytest -q 26 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | apt_packages: 12 | - python3-numpy 13 | tools: 14 | python: "3.9" 15 | 16 | # Optionally build your docs in additional formats such as PDF and ePub 17 | formats: all 18 | 19 | # Build documentation in the docs/ directory with Sphinx 20 | sphinx: 21 | configuration: docs/conf.py 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md") as f: 4 | readme = f.read() 5 | 6 | with open("LICENSE") as f: 7 | license = f.read() 8 | 9 | setup( 10 | name="trussme", 11 | version="0.0.1", 12 | description="Truss construction and analysis", 13 | long_description=readme, 14 | license=license, 15 | author="Christopher McComb", 16 | author_email="ccmcc2012@gmail.com", 17 | url="https://github.com/cmccomb/TrussMe", 18 | install_requires=["numpy", "pandas", "tabulate", "matplotlib", "scipy"], 19 | packages=find_packages(exclude="tests"), 20 | ) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Christopher C. McComb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # trussme 2 | A simple truss solver written in Python. 3 | 4 | Add joints and members, and vary the material and cross-section of specific members in the truss. There is also an option to create truss from .trs file 5 | 6 | Calculate mass, forces, and the factor of safety (FOS) against buckling and yielding. Create a truss analysis report that provides approximate design recommendations 7 | 8 | ## Built-in materials 9 | 10 | Trussme ships with a small library of common engineering materials. Each record 11 | includes a `source` URL citing where the mechanical properties originate. 12 | 13 | - A36_Steel – https://en.wikipedia.org/wiki/A36_steel 14 | - A992_Steel – https://en.wikipedia.org/wiki/ASTM_A992 15 | - 304_Stainless_Steel – https://en.wikipedia.org/wiki/SAE_304_stainless_steel 16 | - 2024_T3_Aluminum – https://en.wikipedia.org/wiki/2024_aluminium_alloy 17 | - 6061_T6_Aluminum – https://en.wikipedia.org/wiki/6061_aluminium_alloy 18 | - 7075_T6_Aluminum – https://en.wikipedia.org/wiki/7075_aluminium_alloy 19 | - Ti_6Al_4V_Titanium – https://en.wikipedia.org/wiki/Ti-6Al-4V 20 | 21 | Custom materials must also provide a provenance `source` when added to a truss. 22 | -------------------------------------------------------------------------------- /tests/test_member_index.py: -------------------------------------------------------------------------------- 1 | """Tests for member addition functions.""" 2 | 3 | import trussme 4 | import pytest 5 | 6 | 7 | def test_add_member_returns_index() -> None: 8 | """Ensure ``add_member`` returns the new member index.""" 9 | # Arrange 10 | truss = trussme.Truss() 11 | start = truss.add_free_joint([0.0, 0.0, 0.0]) 12 | end = truss.add_free_joint([1.0, 0.0, 0.0]) 13 | 14 | # Act 15 | idx = truss.add_member(start, end) 16 | 17 | # Assert 18 | assert idx == 0 19 | 20 | 21 | def test_add_member_invalid_index() -> None: 22 | """``add_member`` should raise when given an invalid joint index.""" 23 | # Arrange 24 | truss = trussme.Truss() 25 | valid = truss.add_free_joint([0.0, 0.0, 0.0]) 26 | _ = truss.add_free_joint([1.0, 0.0, 0.0]) 27 | 28 | # Act / Assert 29 | with pytest.raises(IndexError): 30 | truss.add_member(5, valid) 31 | 32 | 33 | def test_add_member_identical_indices() -> None: 34 | """``add_member`` rejects using the same joint for both ends.""" 35 | # Arrange 36 | truss = trussme.Truss() 37 | joint = truss.add_free_joint([0.0, 0.0, 0.0]) 38 | 39 | # Act / Assert 40 | with pytest.raises(ValueError): 41 | truss.add_member(joint, joint) 42 | -------------------------------------------------------------------------------- /tests/test_material_sources.py: -------------------------------------------------------------------------------- 1 | """Tests for built-in material sources. 2 | 3 | These tests ensure every material in ``MATERIAL_LIBRARY`` specifies a 4 | source URL so that property data can be traced back to its origin. 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | from urllib.parse import urlparse 10 | 11 | from trussme.components import MATERIAL_LIBRARY 12 | 13 | 14 | def test_every_material_has_source() -> None: 15 | """Each material should define a non-empty ``source`` URL.""" 16 | 17 | # Act 18 | sources = [material.get("source", "") for material in MATERIAL_LIBRARY] 19 | 20 | # Assert 21 | for source in sources: 22 | parsed = urlparse(source) 23 | assert parsed.scheme in {"http", "https"} 24 | assert parsed.netloc 25 | 26 | 27 | def test_common_materials_present() -> None: 28 | """Selected materials should ship with the library.""" 29 | 30 | # Arrange 31 | expected = { 32 | "A36_Steel", 33 | "A992_Steel", 34 | "6061_T6_Aluminum", 35 | "7075_T6_Aluminum", 36 | "304_Stainless_Steel", 37 | } 38 | 39 | # Act 40 | names = {material["name"] for material in MATERIAL_LIBRARY} 41 | 42 | # Assert 43 | assert expected.issubset(names) 44 | -------------------------------------------------------------------------------- /trussme/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Trussme: A library for truss analysis and design 3 | 4 | This library includes some utilities and tools for analyzing and designing truss structures. 5 | 6 | Examples 7 | -------- 8 | First, let's construct a small truss 9 | >>> import trussme 10 | >>> small_truss = trussme.Truss() 11 | >>> pin = small_truss.add_pinned_joint([0.0, 0.0, 0.0]) 12 | >>> free = small_truss.add_free_joint([2.5, 2.5, 0.0]) 13 | >>> roller = small_truss.add_roller_joint([5.0, 0.0, 0.0]) 14 | >>> _ = small_truss.add_member(pin, free) 15 | >>> _ = small_truss.add_member(pin, roller) 16 | >>> _ = small_truss.add_member(roller, free) 17 | 18 | Since our truss is planar, its important to add out-of-plane support 19 | >>> small_truss.add_out_of_plane_support("z") 20 | 21 | Let's add a load to the truss 22 | >>> small_truss.set_load(free, [0, -10000, 0]) 23 | 24 | Finally, let's analyze the truss and get the factor of safety and mass 25 | >>> small_truss.analyze() 26 | >>> round(small_truss.fos, 2) 27 | 0.96 28 | >>> round(small_truss.mass, 2) 29 | 22.48 30 | """ 31 | 32 | from trussme.components import ( 33 | MATERIAL_LIBRARY, 34 | Shape, 35 | Material, 36 | Pipe, 37 | Box, 38 | Bar, 39 | Square, 40 | ) 41 | from trussme.truss import Truss, read_trs, read_json, Goals 42 | from trussme.report import report_to_str, report_to_md, print_report 43 | from trussme.optimize import make_truss_generator_function, make_optimization_functions 44 | 45 | __all__ = [ 46 | "Truss", 47 | "read_trs", 48 | "read_json", 49 | "Goals", 50 | "report_to_str", 51 | "report_to_md", 52 | "print_report", 53 | "make_truss_generator_function", 54 | "make_optimization_functions", 55 | "MATERIAL_LIBRARY", 56 | "Shape", 57 | "Material", 58 | "Pipe", 59 | "Box", 60 | "Bar", 61 | "Square", 62 | ] 63 | -------------------------------------------------------------------------------- /tests/test_custom_materials_and_shapes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import trussme 4 | 5 | 6 | class TestCustomStuff(unittest.TestCase): 7 | def test_custom_material(self): 8 | # Build truss from scratch 9 | truss = trussme.Truss() 10 | truss.add_pinned_joint([0.0, 0.0, 0.0]) 11 | truss.add_free_joint([2.5, 2.5, 0.0]) 12 | truss.add_roller_joint([5.0, 0.0, 0.0]) 13 | 14 | truss.add_out_of_plane_support("z") 15 | 16 | truss.joints[1].loads[1] = -20000 17 | 18 | unobtanium: trussme.Material = { 19 | "name": "unobtanium", 20 | "yield_strength": 200_000_000_000, 21 | "elastic_modulus": 200_000_000_000_000.0, 22 | "density": 1_000.0, 23 | "source": "https://en.wikipedia.org/wiki/Unobtainium", 24 | } 25 | 26 | truss.add_member(0, 1, material=unobtanium) 27 | truss.add_member(1, 2, material=unobtanium) 28 | truss.add_member(2, 0, material=unobtanium) 29 | 30 | truss.analyze() 31 | 32 | self.assertIsNotNone(truss.fos) 33 | 34 | def test_custom_shape(self): 35 | # Build truss from scratch 36 | truss = trussme.Truss() 37 | truss.add_pinned_joint([0.0, 0.0, 0.0]) 38 | truss.add_free_joint([2.5, 2.5, 0.0]) 39 | truss.add_roller_joint([5.0, 0.0, 0.0]) 40 | 41 | truss.add_out_of_plane_support("z") 42 | 43 | truss.joints[1].loads[1] = -20000 44 | 45 | class MagicalRod(trussme.Shape): 46 | def __init__(self): 47 | self._params = {} 48 | 49 | def moi(self) -> float: 50 | return 200_000_000_000 51 | 52 | def area(self) -> float: 53 | return 100_000 54 | 55 | def name(self) -> str: 56 | return "magical rod" 57 | 58 | truss.add_member(0, 1, shape=MagicalRod()) 59 | truss.add_member(1, 2, shape=MagicalRod()) 60 | truss.add_member(2, 0, shape=MagicalRod()) 61 | 62 | truss.analyze() 63 | 64 | self.assertIsNotNone(truss.fos) 65 | -------------------------------------------------------------------------------- /tests/example.trs: -------------------------------------------------------------------------------- 1 | # This file defines parameters for a truss. All columns are tab delimited. 2 | # Joints, members, and materials can be included in any order, and lines 3 | # beginning with a "#" symbol are ignored. 4 | 5 | # This block defines materials used in subsequent member definitions. The 6 | # order of hte columns is: 7 | # S name, density, elastic modulus, yield strength, source 8 | S A36_Steel 7850.0 200_000_000_000.0 250_000_000.0 https://en.wikipedia.org/wiki/A36_steel 9 | 10 | # This block defines joints. The order of the columns is 11 | # J X-coord, Y-coord, Z-coord, X-support, Y-support, Z-support 12 | J 0.0 0.0 0.0 1 1 1 13 | J 1.0 0.0 0.0 0 0 1 14 | J 2.0 0.0 0.0 0 0 1 15 | J 3.0 0.0 0.0 0 0 1 16 | J 4.0 0.0 0.0 0 0 1 17 | J 5.0 0.0 0.0 1 1 1 18 | J 0.5 1.0 0.0 0 0 1 19 | J 1.5 1.0 0.0 0 0 1 20 | J 2.5 1.0 0.0 0 0 1 21 | J 3.5 1.0 0.0 0 0 1 22 | J 4.5 1.0 0.0 0 0 1 23 | 24 | # This block defines members. The order of the columns is 25 | # M joint1, joint2, material, shape, {parameters for shape, t=X, etc.} 26 | M 0 1 A36_Steel pipe r=0.02 t=0.002 27 | M 1 2 A36_Steel pipe r=0.02 t=0.002 28 | M 2 3 A36_Steel pipe r=0.02 t=0.002 29 | M 3 4 A36_Steel pipe r=0.02 t=0.002 30 | M 4 5 A36_Steel pipe r=0.02 t=0.002 31 | M 6 7 A36_Steel pipe r=0.02 t=0.002 32 | M 7 8 A36_Steel pipe r=0.02 t=0.002 33 | M 8 9 A36_Steel pipe r=0.02 t=0.002 34 | M 9 10 A36_Steel pipe r=0.02 t=0.002 35 | M 0 6 A36_Steel pipe r=0.02 t=0.002 36 | M 6 1 A36_Steel pipe r=0.02 t=0.002 37 | M 1 7 A36_Steel pipe r=0.02 t=0.002 38 | M 7 2 A36_Steel pipe r=0.02 t=0.002 39 | M 2 8 A36_Steel pipe r=0.02 t=0.002 40 | M 8 3 A36_Steel pipe r=0.02 t=0.002 41 | M 3 9 A36_Steel pipe r=0.02 t=0.002 42 | M 9 4 A36_Steel pipe r=0.02 t=0.002 43 | M 4 10 A36_Steel pipe r=0.02 t=0.002 44 | M 10 5 A36_Steel pipe r=0.02 t=0.002 45 | 46 | # This block defines loads. The order of the columns is: 47 | # L joint, x-load, y-load, z-load 48 | L 7 0 -20000 0 49 | L 8 0 -20000 0 50 | L 9 0 -20000 0 51 | -------------------------------------------------------------------------------- /trussme/visualize.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Any, Optional, Union 2 | 3 | import matplotlib.colors 4 | import matplotlib.pyplot 5 | import numpy 6 | 7 | from matplotlib.figure import Figure 8 | 9 | from trussme.truss import Truss 10 | 11 | MatplotlibColor = Any 12 | """Type: New type to represent a matplotlib color, simply an alias of Any""" 13 | 14 | 15 | def plot_truss( 16 | truss: Truss, 17 | starting_shape: Optional[Union[Literal["fos", "force"], MatplotlibColor]] = "k", 18 | deflected_shape: Optional[Union[Literal["fos", "force"], MatplotlibColor]] = None, 19 | exaggeration_factor: float = 10, 20 | fos_threshold: float = 1.0, 21 | ) -> Figure: 22 | """Plot the truss. 23 | 24 | Parameters 25 | ---------- 26 | truss: Truss 27 | The truss to plot. 28 | starting_shape: None or "fos" or "force" or MatplotlibColor, default="k" 29 | How to show the starting shape. If None, the starting shape is not shown. If "fos", the members are colored 30 | green if the factor of safety is above the threshold and red if it is below. If "force", the members are colored 31 | according to the force in the member. If a color, the members are colored that color. 32 | deflected_shape: None or "fos" or "force" or MatplotlibColor, default = None 33 | How to show the deflected shape. If None, the starting shape is not shown. If "fos", the members are colored 34 | green if the factor of safety is above the threshold and red if it is below. If "force", the members are colored 35 | according to the force in the member. If a color, the members are colored that color. 36 | exaggeration_factor: float, default=10 37 | The factor by which to exaggerate the deflected shape. 38 | fos_threshold: float, default=1.0 39 | The threshold for the factor of safety. If the factor of safety is below this value, the member is colored red. 40 | 41 | Returns 42 | ------- 43 | Figure 44 | A matplotlib figure containing the truss 45 | """ 46 | 47 | fig: Figure = matplotlib.pyplot.figure() 48 | ax = fig.add_subplot( 49 | 111, 50 | ) 51 | 52 | ax.axis("equal") 53 | ax.set_axis_off() 54 | 55 | scaler: float = numpy.max(numpy.abs([member.force for member in truss.members])) 56 | 57 | force_colormap = matplotlib.colors.LinearSegmentedColormap.from_list( 58 | "force", 59 | numpy.array([[1.0, 0.0, 0.0], [0.8, 0.8, 0.8], [0.0, 0.0, 1.0]]), 60 | ) 61 | 62 | for member in truss.members: 63 | start_color: MatplotlibColor 64 | if starting_shape == "fos": 65 | start_color = ( 66 | "g" 67 | if numpy.min([member.fos_buckling, member.fos_yielding]) > fos_threshold 68 | else "r" 69 | ) 70 | elif starting_shape == "force": 71 | start_color = force_colormap(member.force / (2 * scaler) + 0.5) 72 | elif starting_shape is None: 73 | break 74 | else: 75 | start_color = starting_shape 76 | ax.plot( 77 | [member.begin_joint.coordinates[0], member.end_joint.coordinates[0]], 78 | [member.begin_joint.coordinates[1], member.end_joint.coordinates[1]], 79 | color=start_color, 80 | ) 81 | 82 | for member in truss.members: 83 | def_color: MatplotlibColor 84 | if deflected_shape == "fos": 85 | def_color = ( 86 | "g" 87 | if numpy.min([member.fos_buckling, member.fos_yielding]) > fos_threshold 88 | else "r" 89 | ) 90 | elif deflected_shape == "force": 91 | def_color = force_colormap(member.force / (2 * scaler) + 0.5) 92 | elif deflected_shape is None: 93 | break 94 | else: 95 | def_color = deflected_shape 96 | ax.plot( 97 | [ 98 | member.begin_joint.coordinates[0] 99 | + exaggeration_factor * member.begin_joint.deflections[0], 100 | member.end_joint.coordinates[0] 101 | + exaggeration_factor * member.end_joint.deflections[0], 102 | ], 103 | [ 104 | member.begin_joint.coordinates[1] 105 | + exaggeration_factor * member.begin_joint.deflections[1], 106 | member.end_joint.coordinates[1] 107 | + exaggeration_factor * member.end_joint.deflections[1], 108 | ], 109 | color=def_color, 110 | ) 111 | 112 | return fig 113 | -------------------------------------------------------------------------------- /tests/test_construction_and_io.py: -------------------------------------------------------------------------------- 1 | import doctest 2 | import os 3 | import unittest 4 | 5 | import trussme 6 | 7 | TEST_TRUSS_FILENAME = os.path.join(os.path.dirname(__file__), "example.trs") 8 | 9 | 10 | def load_tests(loader, tests, ignore): 11 | tests.addTests(doctest.DocTestSuite(trussme)) 12 | tests.addTests(doctest.DocTestSuite(trussme.truss)) 13 | tests.addTests(doctest.DocTestSuite(trussme.components)) 14 | tests.addTests(doctest.DocTestSuite(trussme.report)) 15 | return tests 16 | 17 | 18 | class TestSequenceFunctions(unittest.TestCase): 19 | def test_demo_report(self): 20 | # Build truss from file 21 | truss_from_file = trussme.read_trs(TEST_TRUSS_FILENAME) 22 | 23 | trussme.report_to_md( 24 | os.path.join(os.path.dirname(__file__), "asdf.md"), 25 | truss_from_file, 26 | trussme.Goals(), 27 | ) 28 | 29 | def test_build_methods(self): 30 | goals = trussme.Goals( 31 | minimum_fos_buckling=1.5, 32 | minimum_fos_yielding=1.5, 33 | maximum_mass=5.0, 34 | maximum_deflection=6e-3, 35 | ) 36 | 37 | # Build truss from scratch 38 | truss_from_commands = trussme.Truss() 39 | truss_from_commands.add_pinned_joint([0.0, 0.0, 0.0]) 40 | truss_from_commands.add_free_joint([1.0, 0.0, 0.0]) 41 | truss_from_commands.add_free_joint([2.0, 0.0, 0.0]) 42 | truss_from_commands.add_free_joint([3.0, 0.0, 0.0]) 43 | truss_from_commands.add_free_joint([4.0, 0.0, 0.0]) 44 | truss_from_commands.add_pinned_joint([5.0, 0.0, 0.0]) 45 | 46 | truss_from_commands.add_free_joint([0.5, 1.0, 0.0]) 47 | truss_from_commands.add_free_joint([1.5, 1.0, 0.0]) 48 | truss_from_commands.add_free_joint([2.5, 1.0, 0.0]) 49 | truss_from_commands.add_free_joint([3.5, 1.0, 0.0]) 50 | truss_from_commands.add_free_joint([4.5, 1.0, 0.0]) 51 | 52 | truss_from_commands.add_out_of_plane_support("z") 53 | 54 | truss_from_commands.joints[7].loads[1] = -20000 55 | truss_from_commands.joints[8].loads[1] = -20000 56 | truss_from_commands.joints[9].loads[1] = -20000 57 | 58 | truss_from_commands.add_member(0, 1) 59 | truss_from_commands.add_member(1, 2) 60 | truss_from_commands.add_member(2, 3) 61 | truss_from_commands.add_member(3, 4) 62 | truss_from_commands.add_member(4, 5) 63 | 64 | truss_from_commands.add_member(6, 7) 65 | truss_from_commands.add_member(7, 8) 66 | truss_from_commands.add_member(8, 9) 67 | truss_from_commands.add_member(9, 10) 68 | 69 | truss_from_commands.add_member(0, 6) 70 | truss_from_commands.add_member(6, 1) 71 | truss_from_commands.add_member(1, 7) 72 | truss_from_commands.add_member(7, 2) 73 | truss_from_commands.add_member(2, 8) 74 | truss_from_commands.add_member(8, 3) 75 | truss_from_commands.add_member(3, 9) 76 | truss_from_commands.add_member(9, 4) 77 | truss_from_commands.add_member(4, 10) 78 | truss_from_commands.add_member(10, 5) 79 | 80 | # Build truss from file 81 | truss_from_file = trussme.read_trs(TEST_TRUSS_FILENAME) 82 | 83 | self.assertEqual( 84 | trussme.report_to_str(truss_from_file, goals), 85 | trussme.report_to_str(truss_from_commands, goals), 86 | ) 87 | 88 | def test_save_to_trs_and_rebuild(self): 89 | goals = trussme.Goals( 90 | minimum_fos_buckling=1.5, 91 | minimum_fos_yielding=1.5, 92 | maximum_mass=5.0, 93 | maximum_deflection=6e-3, 94 | ) 95 | 96 | # Build truss from file 97 | truss_from_file = trussme.read_trs(TEST_TRUSS_FILENAME) 98 | 99 | # Save the truss 100 | truss_from_file.to_trs(os.path.join(os.path.dirname(__file__), "asdf.trs")) 101 | 102 | # Rebuild 103 | truss_rebuilt_from_file = trussme.read_trs( 104 | os.path.join(os.path.dirname(__file__), "asdf.trs") 105 | ) 106 | 107 | self.assertEqual( 108 | trussme.report_to_str(truss_from_file, goals), 109 | trussme.report_to_str(truss_rebuilt_from_file, goals), 110 | ) 111 | 112 | # Cleanup 113 | os.remove(os.path.join(os.path.dirname(__file__), "asdf.trs")) 114 | 115 | def test_save_to_json_and_rebuild(self): 116 | goals = trussme.Goals( 117 | minimum_fos_buckling=1.5, 118 | minimum_fos_yielding=1.5, 119 | maximum_mass=5.0, 120 | maximum_deflection=6e-3, 121 | ) 122 | 123 | # Build truss from file 124 | truss_from_file = trussme.read_trs(TEST_TRUSS_FILENAME) 125 | 126 | # Save the truss 127 | truss_from_file.to_json(os.path.join(os.path.dirname(__file__), "asdf.json")) 128 | 129 | # Rebuild 130 | truss_rebuilt_from_file = trussme.read_json( 131 | os.path.join(os.path.dirname(__file__), "asdf.json") 132 | ) 133 | 134 | self.assertEqual( 135 | trussme.report_to_str(truss_from_file, goals), 136 | trussme.report_to_str(truss_rebuilt_from_file, goals), 137 | ) 138 | 139 | # Cleanup 140 | os.remove(os.path.join(os.path.dirname(__file__), "asdf.json")) 141 | -------------------------------------------------------------------------------- /tests/test_optimization.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | 4 | import scipy.optimize 5 | import numpy 6 | 7 | import trussme 8 | 9 | 10 | class TestCustomStuff(unittest.TestCase): 11 | def test_setup(self): 12 | truss_from_commands = trussme.Truss() 13 | truss_from_commands.add_pinned_joint([0.0, 0.0, 0.0]) 14 | truss_from_commands.add_free_joint([1.0, 0.0, 0.0]) 15 | truss_from_commands.add_free_joint([2.0, 0.0, 0.0]) 16 | truss_from_commands.add_free_joint([3.0, 0.0, 0.0]) 17 | truss_from_commands.add_free_joint([4.0, 0.0, 0.0]) 18 | truss_from_commands.add_pinned_joint([5.0, 0.0, 0.0]) 19 | 20 | truss_from_commands.add_free_joint([0.5, 1.0, 0.0]) 21 | truss_from_commands.add_free_joint([1.5, 1.0, 0.0]) 22 | truss_from_commands.add_free_joint([2.5, 1.0, 0.0]) 23 | truss_from_commands.add_free_joint([3.5, 1.0, 0.0]) 24 | truss_from_commands.add_free_joint([4.5, 1.0, 0.0]) 25 | 26 | truss_from_commands.add_out_of_plane_support("z") 27 | 28 | truss_from_commands.joints[8].loads[1] = -20000 29 | 30 | truss_from_commands.add_member(0, 1) 31 | truss_from_commands.add_member(1, 2) 32 | truss_from_commands.add_member(2, 3) 33 | truss_from_commands.add_member(3, 4) 34 | truss_from_commands.add_member(4, 5) 35 | 36 | truss_from_commands.add_member(6, 7) 37 | truss_from_commands.add_member(7, 8) 38 | truss_from_commands.add_member(8, 9) 39 | truss_from_commands.add_member(9, 10) 40 | 41 | truss_from_commands.add_member(0, 6) 42 | truss_from_commands.add_member(6, 1) 43 | truss_from_commands.add_member(1, 7) 44 | truss_from_commands.add_member(7, 2) 45 | truss_from_commands.add_member(2, 8) 46 | truss_from_commands.add_member(8, 3) 47 | truss_from_commands.add_member(3, 9) 48 | truss_from_commands.add_member(9, 4) 49 | truss_from_commands.add_member(4, 10) 50 | truss_from_commands.add_member(10, 5) 51 | 52 | goals = trussme.Goals() 53 | 54 | x0, obj, con, gen, bnds = trussme.make_optimization_functions( 55 | truss_from_commands, goals 56 | ) 57 | 58 | self.assertEqual( 59 | trussme.report_to_str(gen(x0), goals), 60 | trussme.report_to_str(truss_from_commands, goals), 61 | ) 62 | 63 | def test_joint_optimization(self): 64 | truss_from_commands = trussme.Truss() 65 | truss_from_commands.add_pinned_joint([0.0, 0.0, 0.0]) 66 | truss_from_commands.add_free_joint([1.0, 0.0, 0.0]) 67 | truss_from_commands.add_free_joint([2.0, 0.0, 0.0]) 68 | truss_from_commands.add_free_joint([3.0, 0.0, 0.0]) 69 | truss_from_commands.add_free_joint([4.0, 0.0, 0.0]) 70 | truss_from_commands.add_pinned_joint([5.0, 0.0, 0.0]) 71 | 72 | truss_from_commands.add_free_joint([0.5, 1.0, 0.0]) 73 | truss_from_commands.add_free_joint([1.5, 1.0, 0.0]) 74 | truss_from_commands.add_free_joint([2.5, 1.0, 0.0]) 75 | truss_from_commands.add_free_joint([3.5, 1.0, 0.0]) 76 | truss_from_commands.add_free_joint([4.5, 1.0, 0.0]) 77 | 78 | truss_from_commands.add_out_of_plane_support("z") 79 | 80 | truss_from_commands.joints[8].loads[1] = -20000 81 | 82 | truss_from_commands.add_member(0, 1) 83 | truss_from_commands.add_member(1, 2) 84 | truss_from_commands.add_member(2, 3) 85 | truss_from_commands.add_member(3, 4) 86 | truss_from_commands.add_member(4, 5) 87 | 88 | truss_from_commands.add_member(6, 7) 89 | truss_from_commands.add_member(7, 8) 90 | truss_from_commands.add_member(8, 9) 91 | truss_from_commands.add_member(9, 10) 92 | 93 | truss_from_commands.add_member(0, 6) 94 | truss_from_commands.add_member(6, 1) 95 | truss_from_commands.add_member(1, 7) 96 | truss_from_commands.add_member(7, 2) 97 | truss_from_commands.add_member(2, 8) 98 | truss_from_commands.add_member(8, 3) 99 | truss_from_commands.add_member(3, 9) 100 | truss_from_commands.add_member(9, 4) 101 | truss_from_commands.add_member(4, 10) 102 | truss_from_commands.add_member(10, 5) 103 | 104 | goals = trussme.Goals() 105 | 106 | x0, obj, con, gen, bnds = trussme.make_optimization_functions( 107 | truss_from_commands, 108 | goals, 109 | joint_optimization="full", 110 | member_optimization=None, 111 | ) 112 | 113 | results = scipy.optimize.minimize( 114 | obj, 115 | x0, 116 | constraints=[ 117 | scipy.optimize.NonlinearConstraint( 118 | con, -numpy.inf, 0.0, keep_feasible=True 119 | ) 120 | ], 121 | method="trust-constr", 122 | options={"verbose": 2, "maxiter": 50}, 123 | ) 124 | 125 | result_truss = gen(results.x) 126 | result_truss.analyze() 127 | trussme.report_to_md( 128 | os.path.join(os.path.dirname(__file__), "joint_optim.md"), 129 | result_truss, 130 | goals, 131 | ) 132 | 133 | def test_full_optimization(self): 134 | truss_from_commands = trussme.Truss() 135 | truss_from_commands.add_pinned_joint([0.0, 0.0, 0.0]) 136 | truss_from_commands.add_free_joint([1.0, 0.0, 0.0]) 137 | truss_from_commands.add_free_joint([2.0, 0.0, 0.0]) 138 | truss_from_commands.add_free_joint([3.0, 0.0, 0.0]) 139 | truss_from_commands.add_free_joint([4.0, 0.0, 0.0]) 140 | truss_from_commands.add_pinned_joint([5.0, 0.0, 0.0]) 141 | 142 | truss_from_commands.add_free_joint([0.5, 1.0, 0.0]) 143 | truss_from_commands.add_free_joint([1.5, 1.0, 0.0]) 144 | truss_from_commands.add_free_joint([2.5, 1.0, 0.0]) 145 | truss_from_commands.add_free_joint([3.5, 1.0, 0.0]) 146 | truss_from_commands.add_free_joint([4.5, 1.0, 0.0]) 147 | 148 | truss_from_commands.add_out_of_plane_support("z") 149 | 150 | truss_from_commands.joints[8].loads[1] = -20000 151 | 152 | truss_from_commands.add_member(0, 1) 153 | truss_from_commands.add_member(1, 2) 154 | truss_from_commands.add_member(2, 3) 155 | truss_from_commands.add_member(3, 4) 156 | truss_from_commands.add_member(4, 5) 157 | 158 | truss_from_commands.add_member(6, 7) 159 | truss_from_commands.add_member(7, 8) 160 | truss_from_commands.add_member(8, 9) 161 | truss_from_commands.add_member(9, 10) 162 | 163 | truss_from_commands.add_member(0, 6) 164 | truss_from_commands.add_member(6, 1) 165 | truss_from_commands.add_member(1, 7) 166 | truss_from_commands.add_member(7, 2) 167 | truss_from_commands.add_member(2, 8) 168 | truss_from_commands.add_member(8, 3) 169 | truss_from_commands.add_member(3, 9) 170 | truss_from_commands.add_member(9, 4) 171 | truss_from_commands.add_member(4, 10) 172 | truss_from_commands.add_member(10, 5) 173 | 174 | goals = trussme.Goals() 175 | 176 | x0, obj, con, gen, bnds = trussme.make_optimization_functions( 177 | truss_from_commands, 178 | goals, 179 | joint_optimization="full", 180 | member_optimization="full", 181 | ) 182 | 183 | results = scipy.optimize.minimize( 184 | obj, 185 | x0, 186 | constraints=scipy.optimize.NonlinearConstraint( 187 | con, -numpy.inf, 0.0, keep_feasible=True 188 | ), 189 | bounds=scipy.optimize.Bounds(bnds[0], bnds[1], keep_feasible=True), 190 | method="trust-constr", 191 | options={"verbose": 2, "maxiter": 50}, 192 | ) 193 | 194 | result_truss = gen(results.x) 195 | result_truss.analyze() 196 | trussme.report_to_md( 197 | os.path.join(os.path.dirname(__file__), "full_optim.md"), 198 | result_truss, 199 | goals, 200 | ) 201 | -------------------------------------------------------------------------------- /trussme/report.py: -------------------------------------------------------------------------------- 1 | import json 2 | import io 3 | import re 4 | 5 | import numpy 6 | import pandas 7 | import scipy 8 | from matplotlib.figure import Figure 9 | 10 | import trussme.visualize 11 | 12 | from trussme.truss import Truss, Goals 13 | 14 | 15 | def _fig_to_svg(fig: Figure) -> str: 16 | imgdata = io.StringIO() 17 | fig.savefig(imgdata, format="svg") 18 | imgdata.seek(0) # rewind the data 19 | 20 | svg = imgdata.getvalue() 21 | svg = re.sub("(.*?)", "", svg) 22 | svg = re.sub(r"url\(#(.*?)\)", "url(#truss)", svg) 23 | svg = re.sub('', '', svg) 24 | 25 | return svg 26 | 27 | 28 | def report_to_str(truss: Truss, goals: Goals, with_figures: bool = True) -> str: 29 | """ 30 | Generates a report on the truss 31 | 32 | Parameters 33 | ---------- 34 | truss: Truss 35 | The truss to be reported on 36 | goals: Goals 37 | The goals against which to evaluate the truss 38 | with_figures: bool, default=True 39 | Whether to include figures in the report 40 | 41 | Returns 42 | ------- 43 | str 44 | A full report on the truss 45 | """ 46 | truss.analyze() 47 | 48 | report_string = __generate_summary(truss, goals) + "\n" 49 | report_string += __generate_instantiation_information(truss, with_figures) + "\n" 50 | report_string += __generate_stress_analysis(truss, goals, with_figures) + "\n" 51 | 52 | return report_string 53 | 54 | 55 | def print_report(truss: Truss, goals: Goals) -> None: 56 | """ 57 | Prints a report on the truss 58 | 59 | Parameters 60 | ---------- 61 | truss: Truss 62 | The truss to be reported on 63 | goals: Goals 64 | The goals against which to evaluate the truss 65 | 66 | Returns 67 | ------- 68 | None 69 | """ 70 | print(report_to_str(truss, goals, with_figures=False)) 71 | 72 | 73 | def report_to_md( 74 | file_name: str, truss: Truss, goals: Goals, with_figures: bool = True 75 | ) -> None: 76 | """ 77 | Writes a report in Markdown format 78 | 79 | Parameters 80 | ---------- 81 | file_name: str 82 | The name of the file 83 | truss: Truss 84 | The truss to be reported on 85 | goals: Goals 86 | The goals against which to evaluate the truss 87 | with_figures: bool, default=True 88 | Whether to include figures in the report 89 | 90 | Returns 91 | ------- 92 | None 93 | """ 94 | with open(file_name, "w") as f: 95 | f.write(report_to_str(truss, goals, with_figures=with_figures)) 96 | 97 | 98 | def __generate_summary(truss: Truss, goals: Goals) -> str: 99 | """ 100 | Generate a summary of the analysis. 101 | 102 | Parameters 103 | ---------- 104 | truss: Truss 105 | The truss to be summarized 106 | goals: Goals 107 | The goals against which to evaluate the truss 108 | 109 | Returns 110 | ------- 111 | str 112 | A string containing the summary 113 | """ 114 | summary = "# SUMMARY OF ANALYSIS\n" 115 | summary += ( 116 | "- The truss has a mass of " 117 | + format(truss.mass, ".2f") 118 | + " kg, and a total factor of safety of " 119 | + format(truss.fos, ".2f") 120 | + ".\n" 121 | ) 122 | summary += "- The limit state is " + truss.limit_state + ".\n" 123 | 124 | success_string: list[str] = [] 125 | failure_string: list[str] = [] 126 | 127 | if goals.minimum_fos_buckling < truss.fos_buckling: 128 | success_string.append("buckling FOS") 129 | else: 130 | failure_string.append("buckling FOS") 131 | 132 | if goals.minimum_fos_yielding < truss.fos_yielding: 133 | success_string.append("yielding FOS") 134 | else: 135 | failure_string.append("yielding FOS") 136 | 137 | if goals.maximum_mass > truss.mass: 138 | success_string.append("mass") 139 | else: 140 | failure_string.append("mass") 141 | 142 | if goals.maximum_deflection > truss.deflection: 143 | success_string.append("deflection") 144 | else: 145 | failure_string.append("deflection") 146 | 147 | if len(success_string) != 0: 148 | if len(success_string) == 1: 149 | summary += ( 150 | " The design goal for " + str(success_string[0]) + " was satisfied.\n" 151 | ) 152 | elif len(success_string) == 2: 153 | summary += ( 154 | "- The design goals for " 155 | + str(success_string[0]) 156 | + " and " 157 | + str(success_string[1]) 158 | + " were satisfied.\n" 159 | ) 160 | else: 161 | summary += "- The design goals for " 162 | for st in success_string[0:-1]: 163 | summary += st + ", " 164 | summary += "and " + str(success_string[-1]) + " were satisfied.\n" 165 | 166 | if len(failure_string) != 0: 167 | if len(failure_string) == 1: 168 | summary += ( 169 | "- The design goal for " 170 | + str(failure_string[0]) 171 | + " was not satisfied.\n" 172 | ) 173 | elif len(failure_string) == 2: 174 | summary += ( 175 | "- The design goals for " 176 | + str(failure_string[0]) 177 | + " and " 178 | + str(failure_string[1]) 179 | + " were not satisfied.\n" 180 | ) 181 | else: 182 | summary += "- The design goals for " 183 | for st in failure_string[0:-1]: 184 | summary += st + "," 185 | summary += "and " + str(failure_string[-1]) + " were not satisfied.\n" 186 | 187 | data: list[list[float | str]] = [] 188 | rows: list[str] = [ 189 | "Minimum FOS for Buckling", 190 | "Minimum FOS for Yielding", 191 | "Maximum Mass", 192 | "Maximum Deflection", 193 | ] 194 | data.append( 195 | [ 196 | goals.minimum_fos_buckling, 197 | truss.fos_buckling, 198 | "Yes" if truss.fos_buckling > goals.minimum_fos_buckling else "No", 199 | ] 200 | ) 201 | data.append( 202 | [ 203 | goals.minimum_fos_yielding, 204 | truss.fos_yielding, 205 | "Yes" if truss.fos_yielding > goals.minimum_fos_yielding else "No", 206 | ] 207 | ) 208 | data.append( 209 | [ 210 | goals.maximum_mass, 211 | truss.mass, 212 | "Yes" if truss.mass < goals.maximum_mass else "No", 213 | ] 214 | ) 215 | data.append( 216 | [ 217 | goals.maximum_deflection, 218 | truss.deflection, 219 | "Yes" if truss.deflection < goals.maximum_deflection else "No", 220 | ] 221 | ) 222 | 223 | summary += ( 224 | "\n" 225 | + pandas.DataFrame( 226 | data, 227 | index=rows, 228 | columns=["Target", "Actual", "Ok?"], 229 | ).to_markdown() 230 | ) 231 | 232 | return summary 233 | 234 | 235 | def __generate_instantiation_information( 236 | truss: Truss, with_figures: bool = True 237 | ) -> str: 238 | """ 239 | Generate a summary of the instantiation information. 240 | 241 | Parameters 242 | ---------- 243 | truss: Truss 244 | The truss to be reported on 245 | with_figures: bool, default=True 246 | Whether to include figures in the report 247 | 248 | Returns 249 | ------- 250 | str 251 | A report of the instantiation information 252 | """ 253 | instantiation = "# INSTANTIATION INFORMATION\n" 254 | 255 | if with_figures: 256 | instantiation += _fig_to_svg(trussme.visualize.plot_truss(truss)) + "\n" 257 | 258 | # Print joint information 259 | instantiation += "## JOINTS\n" 260 | joint_data: list[list[str]] = [] 261 | joint_rows: list[str] = [] 262 | for j in truss.joints: 263 | joint_rows.append("Joint_" + "{0:02d}".format(j.idx)) 264 | joint_data.append( 265 | [ 266 | str(j.coordinates[0]), 267 | str(j.coordinates[1]), 268 | str(j.coordinates[2]), 269 | str(j.translation_restricted[0]), 270 | str(j.translation_restricted[1]), 271 | str(j.translation_restricted[2]), 272 | ] 273 | ) 274 | 275 | instantiation += pandas.DataFrame( 276 | joint_data, 277 | index=joint_rows, 278 | columns=["X", "Y", "Z", "X Support?", "Y Support?", "Z Support?"], 279 | ).to_markdown() 280 | 281 | # Print member information 282 | instantiation += "\n## MEMBERS\n" 283 | member_data: list[list[str | float]] = [] 284 | member_rows: list[str] = [] 285 | for m in truss.members: 286 | member_rows.append("Member_" + "{0:02d}".format(m.idx)) 287 | member_data.append( 288 | [ 289 | str(m.begin_joint.idx), 290 | str(m.end_joint.idx), 291 | m.material_name, 292 | m.shape.name(), 293 | json.dumps(m.shape._params) 294 | .replace('"', "") 295 | .replace(": ", "=") 296 | .replace("{", "") 297 | .replace("}", ""), 298 | m.mass, 299 | ] 300 | ) 301 | 302 | instantiation += pandas.DataFrame( 303 | member_data, 304 | index=member_rows, 305 | columns=[ 306 | "Beginning Joint", 307 | "Ending Joint", 308 | "Material", 309 | "Shape", 310 | "Parameters (m)", 311 | "Mass (kg)", 312 | ], 313 | ).to_markdown() 314 | 315 | # Print material list 316 | instantiation += "\n## MATERIALS\n" 317 | material_data: list[list[str]] = [] 318 | material_rows: list[str] = [] 319 | for mat in truss.materials: 320 | material_rows.append(mat["name"]) 321 | material_data.append( 322 | [ 323 | str(mat["density"]), 324 | str(mat["elastic_modulus"] / pow(10, 9)), 325 | str(mat["yield_strength"] / pow(10, 6)), 326 | ] 327 | ) 328 | 329 | instantiation += pandas.DataFrame( 330 | material_data, 331 | index=material_rows, 332 | columns=[ 333 | "Density (kg/m3)", 334 | "Elastic Modulus (GPa)", 335 | "Yield Strength (MPa)", 336 | ], 337 | ).to_markdown() 338 | 339 | return instantiation 340 | 341 | 342 | def __generate_stress_analysis( 343 | truss: Truss, goals: Goals, with_figures: bool = True 344 | ) -> str: 345 | """ 346 | Generate a summary of the stress analysis information. 347 | 348 | Parameters 349 | ---------- 350 | truss: Truss 351 | The truss to be reported on 352 | goals: Goals 353 | The goals against which to evaluate the truss 354 | with_figures: bool, default=True 355 | Whether to include figures in the report 356 | 357 | Returns 358 | ------- 359 | str 360 | A report of the stress analysis information 361 | """ 362 | analysis = "# STRESS ANALYSIS INFORMATION\n" 363 | 364 | # Print information about loads 365 | analysis += "## LOADING\n" 366 | load_data: list[list[str]] = [] 367 | load_rows: list[str] = [] 368 | for j in truss.joints: 369 | load_rows.append("Joint_" + "{0:02d}".format(j.idx)) 370 | load_data.append( 371 | [ 372 | str(j.loads[0] / pow(10, 3)), 373 | format( 374 | ( 375 | j.loads[1] 376 | - sum([m.mass / 2.0 * scipy.constants.g for m in j.members]) 377 | ) 378 | / pow(10, 3), 379 | ".2f", 380 | ), 381 | str(j.loads[2] / pow(10, 3)), 382 | ] 383 | ) 384 | 385 | analysis += pandas.DataFrame( 386 | load_data, index=load_rows, columns=["X Load", "Y Load", "Z Load"] 387 | ).to_markdown() 388 | 389 | # Print information about reactions 390 | analysis += "\n## REACTIONS\n" 391 | reaction_data: list[list[str]] = [] 392 | reaction_rows: list[str] = [] 393 | for j in truss.joints: 394 | reaction_rows.append("Joint_" + "{0:02d}".format(j.idx)) 395 | reaction_data.append( 396 | [ 397 | ( 398 | format(j.reactions[0] / pow(10, 3), ".2f") 399 | if j.translation_restricted[0] != 0.0 400 | else "N/A" 401 | ), 402 | ( 403 | format(j.reactions[1] / pow(10, 3), ".2f") 404 | if j.translation_restricted[1] != 0.0 405 | else "N/A" 406 | ), 407 | ( 408 | format(j.reactions[2] / pow(10, 3), ".2f") 409 | if j.translation_restricted[2] != 0.0 410 | else "N/A" 411 | ), 412 | ] 413 | ) 414 | 415 | analysis += pandas.DataFrame( 416 | reaction_data, 417 | index=reaction_rows, 418 | columns=["X Reaction (kN)", "Y Reaction (kN)", "Z Reaction (kN)"], 419 | ).to_markdown() 420 | 421 | # Print information about members 422 | analysis += "\n## FORCES AND STRESSES\n" 423 | 424 | if with_figures: 425 | analysis += ( 426 | _fig_to_svg(trussme.visualize.plot_truss(truss, starting_shape="force")) 427 | + "\n" 428 | ) 429 | 430 | member_data: list[list[str | float]] = [] 431 | member_rows: list[str] = [] 432 | for m in truss.members: 433 | member_rows.append("Member_" + "{0:02d}".format(m.idx)) 434 | member_data.append( 435 | [ 436 | m.area, 437 | format(m.moment_of_inertia, ".2e"), 438 | format(m.force / pow(10, 3), ".2f"), 439 | m.fos_yielding, 440 | "Yes" if m.fos_yielding > goals.minimum_fos_yielding else "No", 441 | m.fos_buckling if m.fos_buckling > 0 else "N/A", 442 | ( 443 | "Yes" 444 | if m.fos_buckling > goals.minimum_fos_buckling or m.fos_buckling < 0 445 | else "No" 446 | ), 447 | ] 448 | ) 449 | 450 | analysis += pandas.DataFrame( 451 | member_data, 452 | index=member_rows, 453 | columns=[ 454 | "Area (m^2)", 455 | "Moment of Inertia (m^4)", 456 | "Axial force(kN)", 457 | "FOS yielding", 458 | "OK yielding?", 459 | "FOS buckling", 460 | "OK buckling?", 461 | ], 462 | ).to_markdown() 463 | 464 | # Print information about members 465 | analysis += "\n## DEFLECTIONS\n" 466 | 467 | if with_figures: 468 | analysis += ( 469 | _fig_to_svg( 470 | trussme.visualize.plot_truss( 471 | truss, starting_shape="k", deflected_shape="m" 472 | ) 473 | ) 474 | + "\n" 475 | ) 476 | 477 | deflection_data: list[list[str]] = [] 478 | deflection_rows: list[str] = [] 479 | for j in truss.joints: 480 | deflection_rows.append("Joint_" + "{0:02d}".format(j.idx)) 481 | deflection_data.append( 482 | [ 483 | ( 484 | format(j.deflections[0] * pow(10, 3), ".5f") 485 | if j.translation_restricted[0] == 0.0 486 | else "N/A" 487 | ), 488 | ( 489 | format(j.deflections[1] * pow(10, 3), ".5f") 490 | if j.translation_restricted[1] == 0.0 491 | else "N/A" 492 | ), 493 | ( 494 | format(j.deflections[2] * pow(10, 3), ".5f") 495 | if j.translation_restricted[2] == 0.0 496 | else "N/A" 497 | ), 498 | ( 499 | "Yes" 500 | if numpy.linalg.norm(j.deflections) < goals.maximum_deflection 501 | else "No" 502 | ), 503 | ] 504 | ) 505 | 506 | analysis += pandas.DataFrame( 507 | deflection_data, 508 | index=deflection_rows, 509 | columns=[ 510 | "X Deflection(mm)", 511 | "Y Deflection (mm)", 512 | "Z Deflection (mm)", 513 | "OK Deflection?", 514 | ], 515 | ).to_markdown() 516 | 517 | return analysis 518 | -------------------------------------------------------------------------------- /trussme/optimize.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Literal, Optional 2 | 3 | import numpy 4 | 5 | from trussme import Truss, Goals, read_json, Pipe, Box, Square, Bar 6 | 7 | 8 | def make_x0( 9 | truss: Truss, 10 | joint_optimization: Optional[Literal["full"]] = "full", 11 | member_optimization: Optional[Literal["scaled", "full"]] = "full", 12 | ) -> list[float]: 13 | """ 14 | Returns a vector that encodes the current truss design 15 | 16 | Parameters 17 | ---------- 18 | truss: Truss 19 | The truss to configure. 20 | joint_optimization: None or "full", default = "full" 21 | If None, no optimization of joint location. If "full", then full optimization of joint locations will be used. 22 | This will add up to `3n` variables to the optimization vector, where `n` is the number of joints in the truss. 23 | member_optimization: None or "scaled" or "full", default = "full" 24 | If None, no optimization of member cross-section is performed. If "scaled", then member cross-section is 25 | optimally scaled based on initial shape, adding `m` variables to the optimization vector, where `m`, is the 26 | number of members in the truss. If "full", then member cross-section parameters will be separately 27 | optimized, adding up to `km` variables to the optimization vector, where `k` is the number of parameters 28 | defining the cross-section of an individual member. 29 | 30 | Returns 31 | ------- 32 | list[float] 33 | A starting vector that encodes the current truss design 34 | """ 35 | 36 | planar_direction: str = truss.is_planar() 37 | x0: list[float] = [] 38 | 39 | configured_truss = read_json(truss.to_json()) 40 | 41 | if joint_optimization: 42 | for i in range(len(configured_truss.joints)): 43 | if ( 44 | numpy.sum(configured_truss.joints[i].translation_restricted) 45 | == (0 if planar_direction == "none" else 1) 46 | and numpy.sum(configured_truss.joints[i].loads) == 0 47 | ): 48 | if planar_direction != "x": 49 | x0.append(configured_truss.joints[i].coordinates[0]) 50 | if planar_direction != "y": 51 | x0.append(configured_truss.joints[i].coordinates[1]) 52 | if planar_direction != "z": 53 | x0.append(configured_truss.joints[i].coordinates[2]) 54 | 55 | if member_optimization == "scaled": 56 | for i in range(len(configured_truss.members)): 57 | shape_name: str = configured_truss.members[i].shape.name() 58 | if shape_name == "pipe": 59 | x0.append(configured_truss.members[i].shape._params["r"]) 60 | elif shape_name == "box": 61 | x0.append(configured_truss.members[i].shape._params["w"]) 62 | elif shape_name == "bar": 63 | x0.append(configured_truss.members[i].shape._params["r"]) 64 | elif shape_name == "square": 65 | x0.append(configured_truss.members[i].shape._params["w"]) 66 | 67 | if member_optimization == "full": 68 | for i in range(len(configured_truss.members)): 69 | shape_name: str = configured_truss.members[i].shape.name() 70 | if shape_name == "pipe": 71 | x0.append(configured_truss.members[i].shape._params["r"]) 72 | x0.append(configured_truss.members[i].shape._params["t"]) 73 | elif shape_name == "box": 74 | x0.append(configured_truss.members[i].shape._params["w"]) 75 | x0.append(configured_truss.members[i].shape._params["h"]) 76 | x0.append(configured_truss.members[i].shape._params["t"]) 77 | elif shape_name == "bar": 78 | x0.append(configured_truss.members[i].shape._params["r"]) 79 | elif shape_name == "square": 80 | x0.append(configured_truss.members[i].shape._params["w"]) 81 | x0.append(configured_truss.members[i].shape._params["h"]) 82 | 83 | return x0 84 | 85 | 86 | def make_bounds( 87 | truss: Truss, 88 | joint_optimization: Optional[Literal["full"]] = "full", 89 | member_optimization: Optional[Literal["scaled", "full"]] = "full", 90 | ) -> tuple[list[float], list[float]]: 91 | """ 92 | Returns a tuple of vectors that represent lower and upper bounds for the variables of the optimization problem. 93 | 94 | Parameters 95 | ---------- 96 | truss: Truss 97 | The truss to configure. 98 | joint_optimization: None or "full", default = "full" 99 | If None, no bounds are added. If "full", infinite bounds (lower = -inf, upper = inf) are added. This will add 100 | `n` bounds, where `n` is the number of joints in the truss. 101 | member_optimization: None or "scaled" or "full", default = "full" 102 | If None, no bounds are added. If "scaled", then 'm' bounds are added, where `m`, is the number of members in the 103 | truss. If "full", then up to `km` bounds are added, where `k` is the number of parameters defining the 104 | cross-section of an individual member. 105 | 106 | Returns 107 | ------- 108 | list[float] 109 | A starting vector that encodes the current truss design 110 | """ 111 | 112 | planar_direction: str = truss.is_planar() 113 | lb: list[float] = [] 114 | ub: list[float] = [] 115 | 116 | configured_truss = read_json(truss.to_json()) 117 | 118 | if joint_optimization: 119 | for i in range(len(configured_truss.joints)): 120 | if ( 121 | numpy.sum(configured_truss.joints[i].translation_restricted) 122 | == (0 if planar_direction == "none" else 1) 123 | and numpy.sum(configured_truss.joints[i].loads) == 0 124 | ): 125 | if planar_direction != "x": 126 | lb.append(-numpy.inf) 127 | ub.append(numpy.inf) 128 | if planar_direction != "y": 129 | lb.append(-numpy.inf) 130 | ub.append(numpy.inf) 131 | if planar_direction != "z": 132 | lb.append(-numpy.inf) 133 | ub.append(numpy.inf) 134 | 135 | if member_optimization == "scaled": 136 | for i in range(len(configured_truss.members)): 137 | shape_name: str = configured_truss.members[i].shape.name() 138 | if shape_name == "pipe": 139 | lb.append(0.0) 140 | ub.append(numpy.inf) 141 | elif shape_name == "box": 142 | lb.append(0.0) 143 | ub.append(numpy.inf) 144 | elif shape_name == "bar": 145 | lb.append(0.0) 146 | ub.append(numpy.inf) 147 | elif shape_name == "square": 148 | lb.append(0.0) 149 | ub.append(numpy.inf) 150 | 151 | if member_optimization == "full": 152 | for i in range(len(configured_truss.members)): 153 | shape_name: str = configured_truss.members[i].shape.name() 154 | if shape_name == "pipe": 155 | for _ in range(2): 156 | lb.append(0.0) 157 | ub.append(numpy.inf) 158 | elif shape_name == "box": 159 | for _ in range(3): 160 | lb.append(0.0) 161 | ub.append(numpy.inf) 162 | elif shape_name == "bar": 163 | lb.append(0.0) 164 | ub.append(numpy.inf) 165 | elif shape_name == "square": 166 | for _ in range(2): 167 | lb.append(0.0) 168 | ub.append(numpy.inf) 169 | 170 | return lb, ub 171 | 172 | 173 | def make_truss_generator_function( 174 | truss: Truss, 175 | joint_optimization: Optional[Literal["full"]] = "full", 176 | member_optimization: Optional[Literal["scaled", "full"]] = "full", 177 | ) -> Callable[[list[float]], Truss]: 178 | """ 179 | Returns a function that takes a list of floats and returns a truss. 180 | 181 | Parameters 182 | ---------- 183 | truss: Truss 184 | The truss to configure. 185 | joint_optimization: None or "full", default = "full" 186 | If None, no optimization of joint location. If "full", then full optimization of joint locations will be used. 187 | member_optimization: None or "scaled" or "full", default = "full" 188 | If None, no optimization of member cross-section is performed. If "scaled", then member cross-section is 189 | optimally scaled based on initial shape. If "full", then member cross-section parameters will be separately 190 | optimized. 191 | 192 | Returns 193 | ------- 194 | Callable[list[float], Truss] 195 | A function that takes a list of floats and returns a truss. 196 | """ 197 | 198 | planar_direction: str = truss.is_planar() 199 | 200 | def truss_generator(x: list[float]) -> Truss: 201 | configured_truss = read_json(truss.to_json()) 202 | idx = 0 203 | 204 | if joint_optimization: 205 | for i in range(len(configured_truss.joints)): 206 | if ( 207 | numpy.sum(configured_truss.joints[i].translation_restricted) 208 | == (0 if planar_direction == "none" else 1) 209 | and numpy.sum(configured_truss.joints[i].loads) == 0 210 | ): 211 | if planar_direction != "x": 212 | configured_truss.joints[i].coordinates[0] = x[idx] 213 | idx += 1 214 | if planar_direction != "y": 215 | configured_truss.joints[i].coordinates[1] = x[idx] 216 | idx += 1 217 | if planar_direction != "z": 218 | configured_truss.joints[i].coordinates[2] = x[idx] 219 | idx += 1 220 | 221 | if member_optimization == "scaled": 222 | for i in range(len(configured_truss.members)): 223 | shape_name: str = configured_truss.members[i].shape.name() 224 | p = configured_truss.members[i].shape._params 225 | if shape_name == "pipe": 226 | configured_truss.members[i].shape = Pipe( 227 | r=x[idx], t=x[idx] * p["t"] / p["r"] 228 | ) 229 | idx += 1 230 | elif shape_name == "box": 231 | configured_truss.members[i].shape = Box( 232 | w=x[idx], h=x[idx] * p["h"] / p["w"], t=x[idx] * p["t"] / p["w"] 233 | ) 234 | idx += 1 235 | elif shape_name == "bar": 236 | configured_truss.members[i].shape = Bar(r=x[idx]) 237 | idx += 1 238 | elif shape_name == "square": 239 | configured_truss.members[i].shape = Square( 240 | w=x[idx], h=x[idx] * p["h"] / p["w"] 241 | ) 242 | idx += 1 243 | 244 | if member_optimization == "full": 245 | for i in range(len(configured_truss.members)): 246 | shape_name: str = configured_truss.members[i].shape.name() 247 | if shape_name == "pipe": 248 | configured_truss.members[i].shape = Pipe(r=x[idx], t=x[idx + 1]) 249 | idx += 2 250 | elif shape_name == "box": 251 | configured_truss.members[i].shape = Box( 252 | w=x[idx], h=x[idx + 1], t=x[idx + 2] 253 | ) 254 | idx += 3 255 | elif shape_name == "bar": 256 | configured_truss.members[i].shape = Bar(r=x[idx]) 257 | idx += 1 258 | elif shape_name == "square": 259 | configured_truss.members[i].shape = Square(w=x[idx], h=x[idx + 1]) 260 | idx += 2 261 | 262 | return configured_truss 263 | 264 | return truss_generator 265 | 266 | 267 | def make_inequality_constraints( 268 | truss: Truss, 269 | goals: Goals, 270 | joint_optimization: Optional[Literal["full"]] = "full", 271 | member_optimization: Optional[Literal["scaled", "full"]] = "full", 272 | ) -> Callable[[list[float]], list[float]]: 273 | """ 274 | Returns a function that evaluates the inequality constraints. 275 | 276 | Parameters 277 | ---------- 278 | truss: Truss 279 | The truss to configure. 280 | goals: Goals 281 | This informs constraints on yielding FOS, buckling FOS, and deflection. 282 | joint_optimization: Literal[None, "full"], default = "full" 283 | If None, no optimization of joint location. If "full", then full optimization of joint locations will be used. 284 | member_optimization: Literal[None, "scaled", "full"], default = "full" 285 | If None, no optimization of member cross-section is performed. If "scaled", then member cross-section is 286 | optimally scaled based on initial shape. If "full", then member cross-section parameters will be separately 287 | optimized. 288 | 289 | Returns 290 | ------- 291 | Callable[[list[float]], list[float]] 292 | A function that evaluates constraints for the truss 293 | """ 294 | truss_generator = make_truss_generator_function( 295 | truss, joint_optimization, member_optimization 296 | ) 297 | 298 | def inequality_constraints(x: list[float]) -> list[float]: 299 | recon_truss = truss_generator(x) 300 | recon_truss.analyze() 301 | constraints = [ 302 | goals.minimum_fos_buckling - recon_truss.fos_buckling, 303 | goals.minimum_fos_yielding - recon_truss.fos_yielding, 304 | recon_truss.deflection - numpy.min([goals.maximum_deflection, 10000.0]), 305 | ] 306 | if member_optimization == "full": 307 | for i in range(len(recon_truss.members)): 308 | shape_name: str = recon_truss.members[i].shape.name() 309 | if shape_name == "pipe": 310 | constraints.append( 311 | recon_truss.members[i].shape._params["t"] 312 | - recon_truss.members[i].shape._params["r"] 313 | ) 314 | elif shape_name == "box": 315 | constraints.append( 316 | recon_truss.members[i].shape._params["t"] 317 | - recon_truss.members[i].shape._params["w"] 318 | ) 319 | constraints.append( 320 | recon_truss.members[i].shape._params["t"] 321 | - recon_truss.members[i].shape._params["h"] 322 | ) 323 | 324 | return constraints 325 | 326 | return inequality_constraints 327 | 328 | 329 | def make_optimization_functions( 330 | truss: Truss, 331 | goals: Goals, 332 | joint_optimization: Optional[Literal["full"]] = "full", 333 | member_optimization: Optional[Literal["scaled", "full"]] = "full", 334 | ) -> tuple[ 335 | list[float], 336 | Callable[[list[float]], float], 337 | Callable[[list[float]], list[float]], 338 | Callable[[list[float]], Truss], 339 | tuple[list[float], list[float]], 340 | ]: 341 | """ 342 | Creates functions for use in optimization, including a starting vector, objective function, a constraint function, 343 | and a truss generator function. 344 | 345 | Parameters 346 | ---------- 347 | truss: Truss 348 | The truss to use as a starting configuration 349 | goals: Goals 350 | The goals to use for optimization 351 | joint_optimization: Literal[None, "full"] = "full" 352 | If None, no optimization of joint location. If "full", then full optimization of joint locations will be used. 353 | member_optimization: Literal[None, "scaled", "full"] = "full", 354 | Whether to include shape parameters. 355 | 356 | Returns 357 | ------- 358 | tuple[ 359 | list[float], 360 | Callable[[list[float]], float], 361 | Callable[[list[float]], list[float]], 362 | Callable[[list[float]], Truss], 363 | tuple[list[float], list[float]] 364 | ] 365 | A tuple containing the starting vector, objective function, constraint function, and truss generator function. 366 | """ 367 | 368 | x0 = make_x0(truss, joint_optimization, member_optimization) 369 | 370 | truss_generator = make_truss_generator_function( 371 | truss, joint_optimization, member_optimization 372 | ) 373 | 374 | lower_bounds, upper_bounds = make_bounds( 375 | truss, joint_optimization, member_optimization 376 | ) 377 | 378 | inequality_constraints = make_inequality_constraints( 379 | truss, goals, joint_optimization, member_optimization 380 | ) 381 | 382 | def objective_function(x: list[float]) -> float: 383 | return truss_generator(x).mass 384 | 385 | return ( 386 | x0, 387 | objective_function, 388 | inequality_constraints, 389 | truss_generator, 390 | (lower_bounds, upper_bounds), 391 | ) 392 | -------------------------------------------------------------------------------- /trussme/components.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from typing import TypedDict, Literal, cast 3 | 4 | import numpy 5 | from numpy.typing import NDArray 6 | 7 | Material = TypedDict( 8 | "Material", 9 | { 10 | "name": str, 11 | "density": float, 12 | "elastic_modulus": float, 13 | "yield_strength": float, 14 | "source": str, 15 | }, 16 | ) 17 | """TypedDict: New type to contain material properties. 18 | 19 | The ``source`` field stores a URL pointing to the origin of the 20 | mechanical property data for traceability. 21 | """ 22 | 23 | MATERIAL_LIBRARY: list[Material] = [ 24 | { 25 | "name": "A36_Steel", 26 | "density": 7850.0, 27 | "elastic_modulus": 200 * pow(10, 9), 28 | "yield_strength": 250 * pow(10, 6), 29 | "source": "https://en.wikipedia.org/wiki/A36_steel", 30 | }, 31 | { 32 | "name": "A992_Steel", 33 | "density": 7850.0, 34 | "elastic_modulus": 200 * pow(10, 9), 35 | "yield_strength": 345 * pow(10, 6), 36 | "source": "https://en.wikipedia.org/wiki/ASTM_A992", 37 | }, 38 | { 39 | "name": "6061_T6_Aluminum", 40 | "density": 2700.0, 41 | "elastic_modulus": 68.9 * pow(10, 9), 42 | "yield_strength": 276 * pow(10, 6), 43 | "source": "https://en.wikipedia.org/wiki/6061_aluminium_alloy", 44 | }, 45 | { 46 | "name": "7075_T6_Aluminum", 47 | "density": 2810.0, 48 | "elastic_modulus": 71.7 * pow(10, 9), 49 | "yield_strength": 503 * pow(10, 6), 50 | "source": "https://en.wikipedia.org/wiki/7075_aluminium_alloy", 51 | }, 52 | { 53 | "name": "2024_T3_Aluminum", 54 | "density": 2780.0, 55 | "elastic_modulus": 73.1 * pow(10, 9), 56 | "yield_strength": 324 * pow(10, 6), 57 | "source": "https://en.wikipedia.org/wiki/2024_aluminium_alloy", 58 | }, 59 | { 60 | "name": "304_Stainless_Steel", 61 | "density": 8000.0, 62 | "elastic_modulus": 193 * pow(10, 9), 63 | "yield_strength": 215 * pow(10, 6), 64 | "source": "https://en.wikipedia.org/wiki/SAE_304_stainless_steel", 65 | }, 66 | { 67 | "name": "Ti_6Al_4V_Titanium", 68 | "density": 4430.0, 69 | "elastic_modulus": 113.8 * pow(10, 9), 70 | "yield_strength": 880 * pow(10, 6), 71 | "source": "https://en.wikipedia.org/wiki/Ti-6Al-4V", 72 | }, 73 | ] 74 | """list[Material]: List of built-in materials to choose from 75 | """ 76 | 77 | 78 | class Shape(abc.ABC): 79 | """ 80 | Abstract base class for shapes, useful for typehints and creating new shapes 81 | 82 | Examples 83 | -------- 84 | >>> import trussme 85 | >>> class MagicalRod(trussme.Shape): 86 | ... def __init__(self): 87 | ... self._params = {} 88 | ... 89 | ... def moi(self) -> float: 90 | ... return 200_000_000_000 91 | ... 92 | ... def area(self) -> float: 93 | ... return 100_000 94 | ... 95 | ... def name(self) -> str: 96 | ... return "magical rod" 97 | ... 98 | >>> magical_rod = MagicalRod() 99 | >>> magical_rod.moi() 100 | 200000000000 101 | >>> magical_rod.area() 102 | 100000 103 | >>> magical_rod.name() 104 | 'magical rod' 105 | """ 106 | 107 | @abc.abstractmethod 108 | def __init__(self): 109 | self._params = {} 110 | 111 | @abc.abstractmethod 112 | def moi(self) -> float: 113 | """ 114 | The moment of inertia of the shape 115 | 116 | Returns 117 | ------- 118 | float 119 | The moment of inertia of the shape 120 | """ 121 | pass 122 | 123 | @abc.abstractmethod 124 | def area(self) -> float: 125 | """ 126 | The cross-sectional area of the shape 127 | 128 | Returns 129 | ------- 130 | float 131 | The cross-sectional area of the shape 132 | """ 133 | pass 134 | 135 | @abc.abstractmethod 136 | def name(self) -> str: 137 | """ 138 | The name of the shape 139 | 140 | Returns 141 | ------- 142 | str 143 | The name of the shape 144 | """ 145 | pass 146 | 147 | 148 | class Pipe(Shape): 149 | """ 150 | A class to represent a pipe, defined by an outer radius, `r`, and a thickness, `t`. 151 | 152 | Parameters 153 | ---------- 154 | r: float 155 | The outer radius of the pipe 156 | t: float 157 | The thickness of the pipe 158 | 159 | Examples 160 | -------- 161 | >>> import trussme 162 | >>> pipe = trussme.Pipe(r=1.0, t=1.0) 163 | >>> round(pipe.moi(), 3) 164 | 0.785 165 | >>> round(pipe.area(), 3) 166 | 3.142 167 | """ 168 | 169 | def __init__(self, r: float, t: float): 170 | self._params = {"r": r, "t": t} 171 | 172 | def moi(self) -> float: 173 | return (numpy.pi / 4.0) * ( 174 | self._params["r"] ** 4 - (self._params["r"] - self._params["t"]) ** 4 175 | ) 176 | 177 | def area(self) -> float: 178 | return numpy.pi * ( 179 | self._params["r"] ** 2 - (self._params["r"] - self._params["t"]) ** 2 180 | ) 181 | 182 | def name(self) -> str: 183 | return "pipe" 184 | 185 | 186 | class Bar(Shape): 187 | """ 188 | A class to represent a solid round bar, defined by a radius, `r`. 189 | 190 | Parameters 191 | ---------- 192 | r: float 193 | The radius of the bar 194 | 195 | Examples 196 | -------- 197 | >>> import trussme 198 | >>> bar = trussme.Bar(r=1.0) 199 | >>> round(bar.moi(), 3) 200 | 0.785 201 | >>> round(bar.area(), 3) 202 | 3.142 203 | """ 204 | 205 | def __init__(self, r: float): 206 | self._params = {"r": r} 207 | 208 | def moi(self) -> float: 209 | return (numpy.pi / 4.0) * self._params["r"] ** 4 210 | 211 | def area(self) -> float: 212 | return numpy.pi * self._params["r"] ** 2 213 | 214 | def name(self) -> str: 215 | return "bar" 216 | 217 | 218 | class Square(Shape): 219 | """ 220 | A class to represent a square bar, defined by a width, `w`, and a height, `h`. 221 | 222 | Parameters 223 | ---------- 224 | w: float 225 | The width of the bar 226 | h: float 227 | The height of the bar 228 | 229 | Examples 230 | -------- 231 | >>> import trussme 232 | >>> square = trussme.Square(w=1.0, h=1.0) 233 | >>> round(square.moi(), 3) 234 | 0.083 235 | >>> square.area() 236 | 1.0 237 | """ 238 | 239 | def __init__(self, w: float = 0.0, h: float = 0.0): 240 | self._params = {"w": w, "h": h} 241 | 242 | def moi(self) -> float: 243 | if self._params["h"] > self._params["w"]: 244 | return (1.0 / 12.0) * self._params["w"] * self._params["h"] ** 3 245 | else: 246 | return (1.0 / 12.0) * self._params["h"] * self._params["w"] ** 3 247 | 248 | def area(self) -> float: 249 | return self._params["w"] * self._params["h"] 250 | 251 | def name(self) -> str: 252 | return "square" 253 | 254 | 255 | class Box(Shape): 256 | """ 257 | A class to represent a box, defined by a width, `w`, a height, `h`, and a thickness, `t`. 258 | 259 | Parameters 260 | ---------- 261 | w: float 262 | The width of the box 263 | h: float 264 | The height of the box 265 | t: float 266 | The thickness of the box 267 | 268 | Examples 269 | -------- 270 | >>> import trussme 271 | >>> box = trussme.Box(w=1.0, h=1.0, t=0.5) 272 | >>> round(box.moi(), 3) 273 | 0.083 274 | >>> box.area() 275 | 1.0 276 | """ 277 | 278 | def __init__(self, w: float, h: float, t: float): 279 | self._params = {"w": w, "h": h, "t": t} 280 | 281 | def moi(self) -> float: 282 | if self._params["h"] > self._params["w"]: 283 | return (1.0 / 12.0) * (self._params["w"] * self._params["h"] ** 3) - ( 284 | 1.0 / 12.0 285 | ) * (self._params["w"] - 2 * self._params["t"]) * ( 286 | self._params["h"] - 2 * self._params["t"] 287 | ) ** 3 288 | else: 289 | return (1.0 / 12.0) * (self._params["h"] * self._params["w"] ** 3) - ( 290 | 1.0 / 12.0 291 | ) * (self._params["h"] - 2 * self._params["t"]) * ( 292 | self._params["w"] - 2 * self._params["t"] 293 | ) ** 3 294 | 295 | def area(self) -> float: 296 | return self._params["w"] * self._params["h"] - ( 297 | self._params["h"] - 2 * self._params["t"] 298 | ) * (self._params["w"] - 2 * self._params["t"]) 299 | 300 | def name(self) -> str: 301 | return "box" 302 | 303 | 304 | class Joint(object): 305 | """ 306 | A class to represent a joint in a truss 307 | 308 | Parameters 309 | ---------- 310 | coordinates: list[float] 311 | The coordinates of the joint 312 | 313 | Attributes 314 | ---------- 315 | idx: int 316 | The index of the joint 317 | coordinates: list[float] 318 | The coordinates of the joint 319 | translation_restricted: list[bool] 320 | The translation restrictions of the joint 321 | loads: list[float] 322 | The loads on the joint 323 | members: list[Member] 324 | The members connected to the joint 325 | reactions: list[float] 326 | The reactions at the joint 327 | deflections: list[float] 328 | The deflections of the joint 329 | 330 | """ 331 | 332 | def __init__(self, coordinates: list[float]): 333 | # Save the joint id 334 | self.idx: int = 0 335 | 336 | # Coordinates of the joint 337 | self.coordinates = coordinates 338 | 339 | # Restricted translation in x, y, and z 340 | self.translation_restricted: list[bool] = [True, True, True] 341 | 342 | # Loads 343 | self.loads: list[float] = [0.0, 0.0, 0.0] 344 | 345 | # Store connected members 346 | self.members: list[Member] = [] 347 | 348 | # Loads 349 | self.reactions: list[float] = [0.0, 0.0, 0.0] 350 | 351 | # Deflections 352 | self.deflections: list[float] = [0.0, 0.0, 0.0] 353 | 354 | def free(self): 355 | """ 356 | Free translation in all directions 357 | 358 | Returns 359 | ------- 360 | None 361 | """ 362 | self.translation_restricted = [False, False, False] 363 | 364 | def pinned(self): 365 | """ 366 | Restrict translation in all directions, creating a pinned joint 367 | 368 | Returns 369 | ------- 370 | None 371 | """ 372 | # Restrict all translation 373 | self.translation_restricted = [True, True, True] 374 | 375 | def roller(self, constrained_axis: Literal["x", "y", "z"] = "y"): 376 | """ 377 | Free translation in all directions except one, creating a roller joint 378 | 379 | Parameters 380 | ---------- 381 | constrained_axis: str, default="y" 382 | The axis to restrict translation along 383 | 384 | Returns 385 | ------- 386 | None 387 | """ 388 | # Only support reaction along denoted axis 389 | self.translation_restricted = [False, False, False] 390 | if constrained_axis == "x": 391 | self.translation_restricted[0] = True 392 | elif constrained_axis == "y": 393 | self.translation_restricted[1] = True 394 | elif constrained_axis == "z": 395 | self.translation_restricted[2] = True 396 | 397 | def slot(self, free_axis: Literal["x", "y", "z"] = "x"): 398 | """ 399 | Restricted translation in all directions except one, creating a slot joint 400 | 401 | Parameters 402 | ---------- 403 | free_axis: str, default="x" 404 | The axis to allow translation along 405 | 406 | Returns 407 | ------- 408 | None 409 | """ 410 | # Only allow translation along denoted axis 411 | self.translation_restricted = [True, True, True] 412 | if free_axis == "x": 413 | self.translation_restricted[0] = False 414 | elif free_axis == "y": 415 | self.translation_restricted[1] = False 416 | elif free_axis == "z": 417 | self.translation_restricted[2] = False 418 | 419 | 420 | class Member(object): 421 | """ 422 | A class to represent a member in a truss 423 | 424 | Parameters 425 | ---------- 426 | begin_joint: Joint 427 | The joint at the beginning of the member 428 | end_joint: Joint 429 | The joint at the end of the member 430 | material: Material 431 | The material used for the member 432 | shape: Shape 433 | The shape of the member 434 | 435 | Attributes 436 | ---------- 437 | idx: int 438 | The index of the member 439 | shape: Shape 440 | The shape of the member 441 | material: Material 442 | The material used for the member 443 | begin_joint: Joint 444 | The joint at the beginning of the member 445 | end_joint: Joint 446 | The joint at the end of the member 447 | """ 448 | 449 | def __init__( 450 | self, begin_joint: Joint, end_joint: Joint, material: Material, shape: Shape 451 | ): 452 | # Save id number 453 | self.idx: int = 0 454 | 455 | # Shape independent variables 456 | self.shape: Shape = shape 457 | 458 | # Material properties 459 | self.material: Material = material 460 | 461 | # Variables to store information about truss state 462 | self._force: float = 0 463 | 464 | # Variable to store location in truss 465 | self.begin_joint: Joint = begin_joint 466 | self.end_joint: Joint = end_joint 467 | 468 | @property 469 | def yield_strength(self) -> float: 470 | """float: The yield strength of the material used in the member""" 471 | return self.material["yield_strength"] 472 | 473 | @property 474 | def density(self) -> float: 475 | """float: The density of the material used in the member""" 476 | return self.material["density"] 477 | 478 | @property 479 | def elastic_modulus(self) -> float: 480 | """float: The elastic modulus of the material used in the member""" 481 | return self.material["elastic_modulus"] 482 | 483 | @property 484 | def material_name(self) -> str: 485 | """float: The name of the material used in the member""" 486 | return self.material["name"] 487 | 488 | @property 489 | def moment_of_inertia(self) -> float: 490 | """float: The moment of inertia of the shape used for the member""" 491 | return self.shape.moi() 492 | 493 | @property 494 | def area(self) -> float: 495 | """float: The cross-sectional area of the shape used for the member""" 496 | return self.shape.area() 497 | 498 | @property 499 | def linear_mass(self) -> float: 500 | """float: The linear mass of the member""" 501 | return self.area * self.density 502 | 503 | @property 504 | def length(self) -> float: 505 | """float: The length of the member""" 506 | return float( 507 | numpy.linalg.norm( 508 | numpy.array(self.begin_joint.coordinates) 509 | - numpy.array(self.end_joint.coordinates) 510 | ) 511 | ) 512 | 513 | @property 514 | def direction(self) -> NDArray[numpy.float64]: 515 | """NDArray[numpy.float64]: The direction of the member as a unit vector""" 516 | vector_length: NDArray[numpy.float64] = numpy.array( 517 | self.end_joint.coordinates 518 | ) - numpy.array(self.begin_joint.coordinates) 519 | return vector_length / numpy.linalg.norm(vector_length) 520 | 521 | @property 522 | def stiffness(self) -> float: 523 | """float: The axial stiffness of the member""" 524 | return self.elastic_modulus * self.area / self.length 525 | 526 | @property 527 | def stiffness_vector(self) -> NDArray[numpy.float64]: 528 | """NDArray[numpy.float64]: The vector stiffness vector of the member""" 529 | return numpy.multiply(self.stiffness, self.direction) 530 | 531 | @property 532 | def stiffness_matrix(self) -> NDArray[numpy.float64]: 533 | """NDArray[numpy.float64]: The local stiffness matrix of the member""" 534 | d2: NDArray[numpy.float64] = cast( 535 | NDArray[numpy.float64], numpy.outer(self.direction, self.direction) 536 | ) 537 | block = numpy.block([[d2, -d2], [-d2, d2]]).astype(numpy.float64) 538 | return cast(NDArray[numpy.float64], numpy.multiply(self.stiffness, block)) 539 | 540 | @property 541 | def mass(self) -> float: 542 | """float: The total mass of the member""" 543 | return self.length * self.linear_mass 544 | 545 | @property 546 | def force(self) -> float: 547 | """float: The force in the member""" 548 | return self._force 549 | 550 | @force.setter 551 | def force(self, new_force: float): 552 | self._force = new_force 553 | 554 | @property 555 | def fos_yielding(self) -> float: 556 | """float: The factor of safety against yielding""" 557 | return self.yield_strength / abs(self.force / self.area) 558 | 559 | @property 560 | def fos_buckling(self) -> float: 561 | """float: The factor of safety against buckling""" 562 | fos = ( 563 | -( 564 | (numpy.pi**2) 565 | * self.elastic_modulus 566 | * self.moment_of_inertia 567 | / (self.length**2) 568 | ) 569 | / self.force 570 | ) 571 | 572 | return fos if fos > 0 else numpy.inf 573 | -------------------------------------------------------------------------------- /trussme/truss.py: -------------------------------------------------------------------------------- 1 | import dataclasses 2 | from typing import Literal, Union 3 | import json 4 | 5 | import numpy 6 | from numpy.typing import NDArray 7 | import scipy 8 | 9 | from trussme.components import ( 10 | Joint, 11 | Member, 12 | Material, 13 | Pipe, 14 | Bar, 15 | Square, 16 | Shape, 17 | Box, 18 | MATERIAL_LIBRARY, 19 | ) 20 | 21 | 22 | @dataclasses.dataclass 23 | class Goals: 24 | """Container of goals for truss design. 25 | 26 | Attributes 27 | ---------- 28 | minimum_fos_buckling: float, default=1.0 29 | Minimum buckling FOS for the truss, defaults to 1.0 30 | minimum_fos_yielding: float, default=1.0 31 | Minimum yielding FOS for the truss, defaults to 1.0 32 | maximum_mass: float, default=inf 33 | Maximum mass for the truss, defaults to inf 34 | maximum_deflection: float, default=inf 35 | Maximum deflection for the truss, defaults to inf 36 | 37 | Examples 38 | -------- 39 | This is a goal container with the default values 40 | >>> import trussme 41 | >>> import numpy 42 | >>> goals = trussme.Goals( 43 | ... minimum_fos_buckling=1.0, 44 | ... minimum_fos_yielding=1.0, 45 | ... maximum_mass=numpy.inf, 46 | ... maximum_deflection=numpy.inf, 47 | ... ) 48 | """ 49 | 50 | minimum_fos_buckling: float = 1.0 51 | minimum_fos_yielding: float = 1.0 52 | maximum_mass: float = numpy.inf 53 | maximum_deflection: float = numpy.inf 54 | 55 | 56 | class Truss(object): 57 | """The truss class 58 | 59 | Attributes 60 | ---------- 61 | members: list[Member] 62 | A list of all members in the truss 63 | joints: list[Joint] 64 | A list of all joints in the truss 65 | """ 66 | 67 | def __init__(self): 68 | # Make a list to store members in 69 | self.members: list[Member] = [] 70 | 71 | # Make a list to store joints in 72 | self.joints: list[Joint] = [] 73 | 74 | @property 75 | def number_of_members(self) -> int: 76 | """int: Number of members in the truss""" 77 | return len(self.members) 78 | 79 | @property 80 | def number_of_joints(self) -> int: 81 | """int: Number of joints in the truss""" 82 | return len(self.joints) 83 | 84 | @property 85 | def mass(self) -> float: 86 | """float: Total mass of the truss""" 87 | mass = 0 88 | for m in self.members: 89 | mass += m.mass 90 | return mass 91 | 92 | @property 93 | def fos_yielding(self) -> float: 94 | """float: Smallest yielding FOS of any member in the truss""" 95 | return min([m.fos_yielding for m in self.members]) 96 | 97 | @property 98 | def fos_buckling(self) -> float: 99 | """float: Smallest buckling FOS of any member in the truss""" 100 | return min([m.fos_buckling for m in self.members]) 101 | 102 | @property 103 | def fos(self) -> float: 104 | """float: Smallest FOS of any member in the truss""" 105 | return min(self.fos_buckling, self.fos_yielding) 106 | 107 | @property 108 | def deflection(self) -> float: 109 | """float: Largest single joint deflection in the truss""" 110 | return float( 111 | max([numpy.linalg.norm(joint.deflections) for joint in self.joints]) 112 | ) 113 | 114 | @property 115 | def materials(self) -> list[Material]: 116 | """list[Material]: List of unique materials used in the truss""" 117 | material_library: list[Material] = [member.material for member in self.members] 118 | return list({v["name"]: v for v in material_library}.values()) 119 | 120 | @property 121 | def limit_state(self) -> Literal["buckling", "yielding"]: 122 | """Literal["buckling", "yielding"]: The limit state of the truss, either "buckling" or "yielding" """ 123 | if self.fos_buckling < self.fos_yielding: 124 | return "buckling" 125 | else: 126 | return "yielding" 127 | 128 | def is_planar(self) -> Literal["x", "y", "z", "none"]: 129 | """ 130 | Check if the truss is planar 131 | 132 | Returns 133 | ------- 134 | Literal["x", "y", "z", "none"] 135 | The axis along which the truss is planar, or None if it is not planar 136 | """ 137 | 138 | restriction = numpy.prod( 139 | numpy.array([joint.translation_restricted for joint in self.joints]), 140 | axis=0, 141 | ) 142 | 143 | # Check if the truss is planar 144 | if (restriction == [False, False, False]).all(): 145 | return "none" 146 | elif (restriction == [True, False, False]).all(): 147 | return "x" 148 | elif (restriction == [False, True, False]).all(): 149 | return "y" 150 | elif (restriction == [False, False, True]).all(): 151 | return "z" 152 | else: 153 | return "none" 154 | 155 | def add_pinned_joint(self, coordinates: list[float]) -> int: 156 | """Add a pinned joint to the truss at the given coordinates 157 | 158 | Parameters 159 | ---------- 160 | coordinates: list[float] 161 | The coordinates of the joint 162 | 163 | Returns 164 | ------- 165 | int: 166 | The index of the new joint 167 | """ 168 | 169 | # Make the joint 170 | self.joints.append(Joint(coordinates)) 171 | self.joints[-1].pinned() 172 | self.joints[-1].idx = self.number_of_joints - 1 173 | 174 | return self.joints[-1].idx 175 | 176 | def add_roller_joint( 177 | self, coordinates: list[float], constrained_axis: Literal["x", "y", "z"] = "y" 178 | ) -> int: 179 | """ 180 | Add a roller joint to the truss at the given coordinates 181 | 182 | Parameters 183 | ---------- 184 | coordinates: list[float] 185 | The coordinates of the joint 186 | constrained_axis: Literal["x", "y", "z"], default="y" 187 | The axis along which the joint is not allowed to translate 188 | 189 | Returns 190 | ------- 191 | int: 192 | The index of the new joint 193 | """ 194 | 195 | self.joints.append(Joint(coordinates)) 196 | self.joints[-1].roller(constrained_axis=constrained_axis) 197 | self.joints[-1].idx = self.number_of_joints - 1 198 | 199 | return self.joints[-1].idx 200 | 201 | def add_slotted_joint( 202 | self, coordinates: list[float], free_axis: Literal["x", "y", "z"] = "y" 203 | ) -> int: 204 | """ 205 | Add a slotted joint to the truss at the given coordinates 206 | 207 | Parameters 208 | ---------- 209 | coordinates: list[float] 210 | The coordinates of the joint 211 | free_axis: Literal["x", "y", "z"], default="y" 212 | The axis along which the joint is allowed to translate 213 | 214 | Returns 215 | ------- 216 | int: 217 | The index of the new joint 218 | """ 219 | 220 | self.joints.append(Joint(coordinates)) 221 | self.joints[-1].slot(free_axis=free_axis) 222 | self.joints[-1].idx = self.number_of_joints - 1 223 | 224 | return self.joints[-1].idx 225 | 226 | def add_free_joint(self, coordinates: list[float]) -> int: 227 | """ 228 | Add a free joint to the truss at the given coordinates 229 | 230 | Parameters 231 | ---------- 232 | coordinates: list[float] 233 | The coordinates of the joint 234 | 235 | Returns 236 | ------- 237 | int: 238 | The index of the new joint 239 | """ 240 | 241 | # Make the joint 242 | self.joints.append(Joint(coordinates)) 243 | self.joints[-1].free() 244 | self.joints[-1].idx = self.number_of_joints - 1 245 | 246 | return self.joints[-1].idx 247 | 248 | def add_out_of_plane_support( 249 | self, constrained_axis: Literal["x", "y", "z"] = "z" 250 | ) -> None: 251 | for idx in range(self.number_of_joints): 252 | if constrained_axis == "x": 253 | self.joints[idx].translation_restricted[0] = True 254 | elif constrained_axis == "y": 255 | self.joints[idx].translation_restricted[1] = True 256 | elif constrained_axis == "z": 257 | self.joints[idx].translation_restricted[2] = True 258 | 259 | def add_member( 260 | self, 261 | begin_joint_index: int, 262 | end_joint_index: int, 263 | material: Material = MATERIAL_LIBRARY[0], 264 | shape: Shape = Pipe(t=0.002, r=0.02), 265 | ) -> int: 266 | """ 267 | Add a member to the truss 268 | 269 | Parameters 270 | ---------- 271 | begin_joint_index: int 272 | The index of the first joint 273 | end_joint_index: int 274 | The index of the second joint 275 | material: Material, default=material_library[0] 276 | The material of the member 277 | shape: Shape, default=Pipe(t=0.002, r=0.02) 278 | The shape of the member 279 | 280 | Returns 281 | ------- 282 | int 283 | The index of the new member 284 | 285 | Raises 286 | ------ 287 | IndexError 288 | If ``begin_joint_index`` or ``end_joint_index`` are out of range. 289 | ValueError 290 | If ``begin_joint_index`` and ``end_joint_index`` refer to the same joint. 291 | """ 292 | if not 0 <= begin_joint_index < self.number_of_joints: 293 | msg = f"begin_joint_index {begin_joint_index} is out of range" 294 | raise IndexError(msg) 295 | if not 0 <= end_joint_index < self.number_of_joints: 296 | msg = f"end_joint_index {end_joint_index} is out of range" 297 | raise IndexError(msg) 298 | if begin_joint_index == end_joint_index: 299 | raise ValueError("begin_joint_index and end_joint_index must differ") 300 | 301 | member = Member( 302 | self.joints[begin_joint_index], 303 | self.joints[end_joint_index], 304 | material, 305 | shape, 306 | ) 307 | member.idx = self.number_of_members 308 | 309 | # Make a member 310 | self.members.append(member) 311 | 312 | # Update joints 313 | self.joints[begin_joint_index].members.append(self.members[-1]) 314 | self.joints[end_joint_index].members.append(self.members[-1]) 315 | 316 | return member.idx 317 | 318 | def move_joint(self, joint_index: int, coordinates: list[float]) -> None: 319 | """ 320 | Move a joint to the given coordinates 321 | 322 | Parameters 323 | ---------- 324 | joint_index: int 325 | The index of the joint to move 326 | coordinates: list[float] 327 | The coordinates to move the joint to 328 | 329 | Returns 330 | ------- 331 | None 332 | """ 333 | self.joints[joint_index].coordinates = coordinates 334 | 335 | def set_load(self, joint_index: int, load: list[float]) -> None: 336 | """Apply loads to a given joint 337 | Parameters 338 | ---------- 339 | joint_index: int 340 | The index of the joint to apply the load to 341 | load: list[float] 342 | The load to apply to the joint 343 | Returns 344 | ------- 345 | None 346 | """ 347 | 348 | self.joints[joint_index].loads = load 349 | 350 | @property 351 | def __load_matrix(self) -> NDArray[float]: 352 | loads = numpy.zeros([3, self.number_of_joints]) 353 | for i in range(self.number_of_joints): 354 | loads[0, i] = self.joints[i].loads[0] 355 | loads[1, i] = self.joints[i].loads[1] - sum( 356 | [ 357 | member.mass / 2.0 * scipy.constants.g 358 | for member in self.joints[i].members 359 | ] 360 | ) 361 | loads[2, i] = self.joints[i].loads[2] 362 | 363 | return loads 364 | 365 | @property 366 | def __connection_matrix(self) -> NDArray[float]: 367 | return numpy.array( 368 | [[member.begin_joint.idx, member.end_joint.idx] for member in self.members] 369 | ).T 370 | 371 | def analyze(self) -> None: 372 | """ 373 | Analyze the truss 374 | 375 | Returns 376 | ------- 377 | None 378 | 379 | """ 380 | loads = self.__load_matrix 381 | connections = self.__connection_matrix 382 | reactions = numpy.array( 383 | [joint.translation_restricted for joint in self.joints] 384 | ).T 385 | 386 | tj: NDArray[float] = numpy.zeros([3, self.number_of_members]) 387 | dof: NDArray[float] = numpy.zeros( 388 | [3 * self.number_of_joints, 3 * self.number_of_joints] 389 | ) 390 | deflections: NDArray[float] = numpy.ones([3, self.number_of_joints]) 391 | deflections -= reactions 392 | 393 | # This identifies joints that can be loaded 394 | ff: NDArray[float] = numpy.where(deflections.T.flat == 1)[0] 395 | 396 | for idx, member in enumerate(self.members): 397 | ss = member.stiffness_matrix 398 | tj[:, idx] = member.stiffness_vector 399 | 400 | e = list( 401 | range((3 * member.begin_joint.idx), (3 * member.begin_joint.idx + 3)) 402 | ) + list(range((3 * member.end_joint.idx), (3 * member.end_joint.idx + 3))) 403 | for ii in range(6): 404 | for j in range(6): 405 | dof[e[ii], e[j]] += ss[ii, j] 406 | 407 | ssff = numpy.zeros([len(ff), len(ff)]) 408 | for i in range(len(ff)): 409 | for j in range(len(ff)): 410 | ssff[i, j] = dof[ff[i], ff[j]] 411 | 412 | flat_loads = loads.T.flat[ff] 413 | flat_deflections = numpy.linalg.solve(ssff, flat_loads) 414 | 415 | ff = numpy.where(deflections.T == 1) 416 | for i in range(len(ff[0])): 417 | deflections[ff[1][i], ff[0][i]] = flat_deflections[i] 418 | 419 | # Compute the reactions 420 | reactions = ( 421 | numpy.sum(dof * deflections.T.flat[:], axis=1) 422 | .reshape([self.number_of_joints, 3]) 423 | .T 424 | ) 425 | 426 | # Store the results 427 | for i in range(self.number_of_joints): 428 | for j in range(3): 429 | if self.joints[i].translation_restricted[j]: 430 | self.joints[i].reactions[j] = float(reactions[j, i]) 431 | self.joints[i].deflections[j] = 0.0 432 | else: 433 | self.joints[i].reactions[j] = 0.0 434 | self.joints[i].deflections[j] = float(deflections[j, i]) 435 | 436 | # Calculate member forces and store the results 437 | forces = numpy.sum( 438 | numpy.multiply( 439 | tj, 440 | deflections[:, connections[1, :]] - deflections[:, connections[0, :]], 441 | ), 442 | axis=0, 443 | ) 444 | # Store the results 445 | for i in range(self.number_of_members): 446 | self.members[i].force = forces[i] 447 | 448 | return None 449 | 450 | def to_json(self, file_name: Union[None, str] = None) -> Union[str, None]: 451 | """ 452 | Saves the truss to a JSON file 453 | 454 | Parameters 455 | ---------- 456 | file_name: Union[None, str] 457 | The filename to use for the JSON file. If None, the json is returned as a string 458 | 459 | Returns 460 | ------- 461 | Union[str, None] 462 | """ 463 | 464 | class JointEncoder(json.JSONEncoder): 465 | def default(self, obj): 466 | if isinstance(obj, Joint): 467 | return { 468 | "coordinates": obj.coordinates, 469 | "loads": obj.loads, 470 | "translation": obj.translation_restricted, 471 | } 472 | # Let the base class default method raise the TypeError 473 | return json.JSONEncoder.default(self, obj) 474 | 475 | class MemberEncoder(json.JSONEncoder): 476 | def default(self, obj): 477 | if isinstance(obj, Member): 478 | return { 479 | "begin_joint": obj.begin_joint.idx, 480 | "end_joint": obj.end_joint.idx, 481 | "material": obj.material["name"], 482 | "shape": { 483 | "name": obj.shape.name(), 484 | "parameters": obj.shape._params, 485 | }, 486 | } 487 | # Let the base class default method raise the TypeError 488 | return json.JSONEncoder.default(self, obj) 489 | 490 | materials = json.dumps(self.materials, indent=4) 491 | joints = json.dumps(self.joints, indent=4, cls=JointEncoder) 492 | members = json.dumps(self.members, indent=4, cls=MemberEncoder) 493 | 494 | combined = { 495 | "materials": json.loads(materials), 496 | "joints": json.loads(joints), 497 | "members": json.loads(members), 498 | } 499 | 500 | if file_name is None: 501 | return json.dumps(combined) 502 | else: 503 | with open(file_name, "w") as f: 504 | json.dump(combined, f, indent=4) 505 | return None 506 | 507 | def to_trs(self, file_name: str) -> None: 508 | """ 509 | Saves the truss to a .trs file 510 | 511 | Parameters 512 | ---------- 513 | file_name: str 514 | The filename to use for the truss file 515 | 516 | Returns 517 | ------- 518 | None 519 | """ 520 | 521 | with open(file_name, "w") as f: 522 | # Do materials 523 | for material in self.materials: 524 | f.write( 525 | "S" 526 | + "\t" 527 | + str(material["name"]) 528 | + "\t" 529 | + str(material["density"]) 530 | + "\t" 531 | + str(material["elastic_modulus"]) 532 | + "\t" 533 | + str(material["yield_strength"]) 534 | + "\t" 535 | + material["source"] 536 | + "\n" 537 | ) 538 | 539 | # Do the joints 540 | load_string = "" 541 | for j in self.joints: 542 | f.write( 543 | "J" 544 | + "\t" 545 | + str(j.coordinates[0]) 546 | + "\t" 547 | + str(j.coordinates[1]) 548 | + "\t" 549 | + str(j.coordinates[2]) 550 | + "\t" 551 | + str(int(j.translation_restricted[0])) 552 | + "\t" 553 | + str(int(j.translation_restricted[1])) 554 | + "\t" 555 | + str(int(j.translation_restricted[2])) 556 | + "\n" 557 | ) 558 | if numpy.sum(j.loads) != 0: 559 | load_string += "L" + "\t" 560 | load_string += str(j.idx) + "\t" 561 | load_string += str(j.loads[0]) + "\t" 562 | load_string += str(j.loads[1]) + "\t" 563 | load_string += str(j.loads[2]) + "\t" 564 | load_string += "\n" 565 | 566 | # Do the members 567 | for m in self.members: 568 | f.write( 569 | "M" 570 | + "\t" 571 | + str(m.begin_joint.idx) 572 | + "\t" 573 | + str(m.end_joint.idx) 574 | + "\t" 575 | + m.material["name"] 576 | + "\t" 577 | + m.shape.name() 578 | + "\t" 579 | ) 580 | for key in m.shape._params.keys(): 581 | f.write(key + "=" + str(m.shape._params[key]) + "\t") 582 | f.write("\n") 583 | 584 | # Do the loads 585 | f.write(load_string) 586 | 587 | 588 | def read_trs(file_name: str) -> Truss: 589 | """ 590 | Read a .trs file and return a Truss object 591 | 592 | Parameters 593 | ---------- 594 | file_name: str 595 | The name of the .trs file to be read 596 | 597 | Returns 598 | ------- 599 | Truss 600 | The object loaded from the .trs file 601 | """ 602 | truss = Truss() 603 | material_library: list[Material] = [] 604 | 605 | with open(file_name, "r") as f: 606 | for idx, line in enumerate(f): 607 | if line[0] == "S": 608 | info = line.split()[1:] 609 | material_library.append( 610 | { 611 | "name": info[0], 612 | "density": float(info[1]), 613 | "elastic_modulus": float(info[2]), 614 | "yield_strength": float(info[3]), 615 | "source": info[4] if len(info) > 4 else "", 616 | } 617 | ) 618 | 619 | elif line[0] == "J": 620 | info = line.split()[1:] 621 | truss.add_free_joint([float(x) for x in info[:3]]) 622 | truss.joints[-1].translation_restricted = [ 623 | bool(int(x)) for x in info[3:] 624 | ] 625 | elif line[0] == "M": 626 | info = line.split()[1:] 627 | material = next( 628 | item for item in material_library if item["name"] == info[2] 629 | ) 630 | 631 | # Parse parameters 632 | ks = [] 633 | vs = [] 634 | for param in range(4, len(info)): 635 | kvpair = info[param].split("=") 636 | ks.append(kvpair[0]) 637 | vs.append(float(kvpair[1])) 638 | if info[3] == "pipe": 639 | shape = Pipe(**dict(zip(ks, vs))) 640 | elif info[3] == "bar": 641 | shape = Bar(**dict(zip(ks, vs))) 642 | elif info[3] == "square": 643 | shape = Square(**dict(zip(ks, vs))) 644 | elif info[3] == "box": 645 | shape = Box(**dict(zip(ks, vs))) 646 | truss.add_member(int(info[0]), int(info[1]), material, shape) 647 | 648 | elif line[0] == "L": 649 | info = line.split()[1:] 650 | truss.joints[int(info[0])].loads[0] = float(info[1]) 651 | truss.joints[int(info[0])].loads[1] = float(info[2]) 652 | truss.joints[int(info[0])].loads[2] = float(info[3]) 653 | elif line[0] != "#" and not line.isspace(): 654 | raise ValueError("'" + line[0] + "' is not a valid line initializer.") 655 | 656 | return truss 657 | 658 | 659 | def read_json(file_name: str) -> Truss: 660 | """ 661 | Read a JSON file and return a Truss object 662 | 663 | Parameters 664 | ---------- 665 | file_name: str 666 | The name of the JSON file to be read, or a valid JSON string 667 | 668 | Returns 669 | ------- 670 | Truss 671 | The object loaded from the JSON file 672 | """ 673 | try: 674 | json_truss = json.loads(file_name) 675 | except ValueError: 676 | with open(file_name, "r") as file: 677 | json_truss = json.load(file) 678 | 679 | truss = Truss() 680 | current_material_library: list[Material] = json_truss["materials"] 681 | 682 | for joint in json_truss["joints"]: 683 | truss.add_free_joint(joint["coordinates"]) 684 | truss.joints[-1].translation_restricted = joint["translation"] 685 | truss.joints[-1].loads = joint["loads"] 686 | 687 | for member in json_truss["members"]: 688 | material: Material = next( 689 | item 690 | for item in current_material_library 691 | if item["name"] == member["material"] 692 | ) 693 | shape_params = member["shape"]["parameters"] 694 | if member["shape"]["name"] == "pipe": 695 | shape = Pipe(**dict(shape_params)) 696 | elif member["shape"]["name"] == "bar": 697 | shape = Bar(**dict(shape_params)) 698 | elif member["shape"]["name"] == "square": 699 | shape = Square(**dict(shape_params)) 700 | elif member["shape"]["name"] == "box": 701 | shape = Box(**dict(shape_params)) 702 | else: 703 | raise ValueError( 704 | "Shape type '" 705 | + member["shape"]["name"] 706 | + "' is a custom type and not supported." 707 | ) 708 | truss.add_member( 709 | member["begin_joint"], member["end_joint"], material=material, shape=shape 710 | ) 711 | 712 | return truss 713 | -------------------------------------------------------------------------------- /examples/optimizing_for_mass.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "id": "view-in-github", 7 | "colab_type": "text" 8 | }, 9 | "source": [ 10 | "\"Open" 11 | ] 12 | }, 13 | { 14 | "cell_type": "markdown", 15 | "source": [ 16 | "# Install TrussMe" 17 | ], 18 | "metadata": { 19 | "id": "d4JweabFv5bI" 20 | } 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": 1, 25 | "metadata": { 26 | "colab": { 27 | "base_uri": "https://localhost:8080/" 28 | }, 29 | "id": "lIYdn1woOS1n", 30 | "outputId": "fe0ad85a-65e1-4d1f-8c53-c270b12615b6" 31 | }, 32 | "outputs": [ 33 | { 34 | "output_type": "stream", 35 | "name": "stdout", 36 | "text": [ 37 | " Preparing metadata (setup.py) ... \u001b[?25l\u001b[?25hdone\n", 38 | " Building wheel for trussme (setup.py) ... \u001b[?25l\u001b[?25hdone\n" 39 | ] 40 | } 41 | ], 42 | "source": [ 43 | "!pip install git+https://github.com/cmccomb/trussme.git -qqq" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "source": [ 49 | "# Set up functions for optimization" 50 | ], 51 | "metadata": { 52 | "id": "XXEb3-8Zv8hC" 53 | } 54 | }, 55 | { 56 | "cell_type": "code", 57 | "source": [ 58 | "import trussme\n", 59 | "import scipy.optimize\n", 60 | "import numpy" 61 | ], 62 | "metadata": { 63 | "id": "wSB4taeLfbYt" 64 | }, 65 | "execution_count": 2, 66 | "outputs": [] 67 | }, 68 | { 69 | "cell_type": "markdown", 70 | "source": [ 71 | "First, let's construct a truss that we will use as a starting point." 72 | ], 73 | "metadata": { 74 | "id": "-42VVeJkv-k-" 75 | } 76 | }, 77 | { 78 | "cell_type": "code", 79 | "source": [ 80 | "truss_from_commands = trussme.Truss()\n", 81 | "truss_from_commands.add_pinned_joint([0.0, 0.0, 0.0])\n", 82 | "truss_from_commands.add_free_joint([1.0, 0.0, 0.0])\n", 83 | "truss_from_commands.add_free_joint([2.0, 0.0, 0.0])\n", 84 | "truss_from_commands.add_free_joint([3.0, 0.0, 0.0])\n", 85 | "truss_from_commands.add_free_joint([4.0, 0.0, 0.0])\n", 86 | "truss_from_commands.add_pinned_joint([5.0, 0.0, 0.0])\n", 87 | "\n", 88 | "truss_from_commands.add_free_joint([0.5, 1.0, 0.0])\n", 89 | "truss_from_commands.add_free_joint([1.5, 1.0, 0.0])\n", 90 | "truss_from_commands.add_free_joint([2.5, 1.0, 0.0])\n", 91 | "truss_from_commands.add_free_joint([3.5, 1.0, 0.0])\n", 92 | "truss_from_commands.add_free_joint([4.5, 1.0, 0.0])\n", 93 | "\n", 94 | "truss_from_commands.add_out_of_plane_support(\"z\")\n", 95 | "\n", 96 | "truss_from_commands.joints[8].loads[1] = -20000\n", 97 | "\n", 98 | "truss_from_commands.add_member(0, 1)\n", 99 | "truss_from_commands.add_member(1, 2)\n", 100 | "truss_from_commands.add_member(2, 3)\n", 101 | "truss_from_commands.add_member(3, 4)\n", 102 | "truss_from_commands.add_member(4, 5)\n", 103 | "\n", 104 | "truss_from_commands.add_member(6, 7)\n", 105 | "truss_from_commands.add_member(7, 8)\n", 106 | "truss_from_commands.add_member(8, 9)\n", 107 | "truss_from_commands.add_member(9, 10)\n", 108 | "\n", 109 | "truss_from_commands.add_member(0, 6)\n", 110 | "truss_from_commands.add_member(6, 1)\n", 111 | "truss_from_commands.add_member(1, 7)\n", 112 | "truss_from_commands.add_member(7, 2)\n", 113 | "truss_from_commands.add_member(2, 8)\n", 114 | "truss_from_commands.add_member(8, 3)\n", 115 | "truss_from_commands.add_member(3, 9)\n", 116 | "truss_from_commands.add_member(9, 4)\n", 117 | "truss_from_commands.add_member(4, 10)\n", 118 | "truss_from_commands.add_member(10, 5)" 119 | ], 120 | "metadata": { 121 | "id": "CItMgcAUfcWs" 122 | }, 123 | "execution_count": 3, 124 | "outputs": [] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "source": [ 129 | "goals = trussme.Goals(\n", 130 | " minimum_fos_buckling = 1.0,\n", 131 | " minimum_fos_yielding = 1.0,\n", 132 | " maximum_deflection = 0.01\n", 133 | ")" 134 | ], 135 | "metadata": { 136 | "id": "x_1HQpQIzA0z" 137 | }, 138 | "execution_count": 4, 139 | "outputs": [] 140 | }, 141 | { 142 | "cell_type": "code", 143 | "source": [ 144 | "truss_from_commands.analyze()\n", 145 | "print(\n", 146 | " \"mass = {:.2f} kg; \".format(truss_from_commands.mass),\n", 147 | " \"FOS = {:.2f}; \".format(truss_from_commands.fos),\n", 148 | " \"deflection = {:.2f} cm; \".format(truss_from_commands.deflection*100)\n", 149 | ")\n", 150 | "trussme.visualize.plot_truss(truss_from_commands, starting_shape=\"force\");" 151 | ], 152 | "metadata": { 153 | "id": "cc6n_YmqyGmQ", 154 | "outputId": "0d40152f-6692-4d82-c7aa-dac18e33beab", 155 | "colab": { 156 | "base_uri": "https://localhost:8080/", 157 | "height": 423 158 | } 159 | }, 160 | "execution_count": 5, 161 | "outputs": [ 162 | { 163 | "output_type": "stream", 164 | "name": "stdout", 165 | "text": [ 166 | "mass = 37.58 kg; FOS = 2.95; deflection = 0.28 cm; \n" 167 | ] 168 | }, 169 | { 170 | "output_type": "display_data", 171 | "data": { 172 | "text/plain": [ 173 | "
" 174 | ], 175 | "image/png": "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ANplDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABRnDABAccYAABT3f6+wDzlKVmNOAAAAAElFTkSuQmCC\n" 176 | }, 177 | "metadata": {} 178 | } 179 | ] 180 | }, 181 | { 182 | "cell_type": "markdown", 183 | "source": [ 184 | "# Mass Minimization by Joint Coordinates\n", 185 | "Next, let's generate a mass-minimal truss by moving the joints around only" 186 | ], 187 | "metadata": { 188 | "id": "UK3HQm1bxteU" 189 | } 190 | }, 191 | { 192 | "cell_type": "code", 193 | "source": [ 194 | "x0, objective, constraints, generate_truss, bounds = trussme.make_optimization_functions(truss_from_commands, goals, joint_coordinates=True, shape_parameters=False)" 195 | ], 196 | "metadata": { 197 | "id": "yBoTNKg9gp9v" 198 | }, 199 | "execution_count": 6, 200 | "outputs": [] 201 | }, 202 | { 203 | "cell_type": "code", 204 | "source": [ 205 | "results = scipy.optimize.minimize(\n", 206 | " objective,\n", 207 | " x0,\n", 208 | " constraints=scipy.optimize.NonlinearConstraint(constraints, -numpy.inf, 0.0, keep_feasible=True),\n", 209 | " bounds=scipy.optimize.Bounds(bounds[0], bounds[1], keep_feasible=True),\n", 210 | " method='trust-constr',\n", 211 | " options={\n", 212 | " \"verbose\": 2,\n", 213 | " \"maxiter\": 100,\n", 214 | " }\n", 215 | ")" 216 | ], 217 | "metadata": { 218 | "id": "agtKz9YHq-er", 219 | "colab": { 220 | "base_uri": "https://localhost:8080/" 221 | }, 222 | "outputId": "87dc6836-f5c0-46e9-8697-575028266be8" 223 | }, 224 | "execution_count": 7, 225 | "outputs": [ 226 | { 227 | "output_type": "stream", 228 | "name": "stdout", 229 | "text": [ 230 | "| niter |f evals|CG iter| obj func |tr radius | opt | c viol |\n", 231 | "|-------|-------|-------|-------------|----------|----------|----------|\n", 232 | "| 1 | 17 | 0 | +3.7583e+01 | 1.00e+00 | 3.32e+00 | 0.00e+00 |\n", 233 | "| 2 | 34 | 1 | +2.9515e+01 | 7.00e+00 | 2.97e+00 | 0.00e+00 |\n", 234 | "| 3 | 51 | 7 | +2.9515e+01 | 1.72e+00 | 2.97e+00 | 0.00e+00 |\n", 235 | "| 4 | 68 | 13 | +2.9515e+01 | 1.72e-01 | 2.97e+00 | 0.00e+00 |\n", 236 | "| 5 | 85 | 15 | +2.8458e+01 | 1.20e+00 | 2.67e+00 | 0.00e+00 |\n", 237 | "| 6 | 102 | 24 | +2.8458e+01 | 1.20e-01 | 2.67e+00 | 0.00e+00 |\n", 238 | "| 7 | 119 | 26 | +2.7795e+01 | 8.41e-01 | 2.27e+00 | 0.00e+00 |\n", 239 | "| 8 | 136 | 35 | +2.7795e+01 | 8.41e-02 | 2.27e+00 | 0.00e+00 |\n", 240 | "| 9 | 153 | 38 | +2.7531e+01 | 5.89e-01 | 2.12e+00 | 0.00e+00 |\n", 241 | "| 10 | 170 | 49 | +2.6950e+01 | 3.37e+00 | 2.77e+00 | 0.00e+00 |\n", 242 | "| 11 | 187 | 61 | +2.6732e+01 | 3.37e+00 | 3.20e+00 | 0.00e+00 |\n", 243 | "| 12 | 204 | 66 | +2.6572e+01 | 3.37e+00 | 3.17e+00 | 0.00e+00 |\n", 244 | "| 13 | 221 | 71 | +2.6526e+01 | 3.37e+00 | 3.31e+00 | 0.00e+00 |\n", 245 | "| 14 | 238 | 86 | +2.6065e+01 | 3.37e+00 | 3.72e+00 | 0.00e+00 |\n", 246 | "| 15 | 255 | 97 | +2.5805e+01 | 3.37e+00 | 2.62e+00 | 0.00e+00 |\n", 247 | "| 16 | 272 | 108 | +2.5692e+01 | 3.37e+00 | 3.21e+00 | 0.00e+00 |\n", 248 | "| 17 | 289 | 114 | +2.5385e+01 | 3.37e+00 | 2.92e+00 | 0.00e+00 |\n", 249 | "| 18 | 306 | 123 | +2.5219e+01 | 3.37e+00 | 3.78e+00 | 0.00e+00 |\n", 250 | "| 19 | 323 | 136 | +2.4910e+01 | 3.37e+00 | 3.25e+00 | 0.00e+00 |\n", 251 | "| 20 | 340 | 146 | +2.4558e+01 | 3.37e+00 | 2.94e+00 | 0.00e+00 |\n", 252 | "| 21 | 357 | 161 | +2.4558e+01 | 3.37e-01 | 2.94e+00 | 0.00e+00 |\n", 253 | "| 22 | 374 | 168 | +2.3647e+01 | 2.36e+00 | 4.23e+00 | 0.00e+00 |\n", 254 | "| 23 | 391 | 178 | +2.3647e+01 | 2.36e-01 | 4.23e+00 | 0.00e+00 |\n", 255 | "| 24 | 408 | 185 | +2.3268e+01 | 1.65e+00 | 3.75e+00 | 0.00e+00 |\n", 256 | "| 25 | 425 | 200 | +2.3268e+01 | 1.65e-01 | 3.75e+00 | 0.00e+00 |\n", 257 | "| 26 | 442 | 206 | +2.3063e+01 | 1.16e+00 | 3.63e+00 | 0.00e+00 |\n", 258 | "| 27 | 459 | 222 | +2.3063e+01 | 1.16e-01 | 3.63e+00 | 0.00e+00 |\n", 259 | "| 28 | 476 | 229 | +2.2889e+01 | 8.09e-01 | 3.80e+00 | 0.00e+00 |\n", 260 | "| 29 | 493 | 238 | +2.2889e+01 | 8.09e-02 | 3.80e+00 | 0.00e+00 |\n", 261 | "| 30 | 510 | 245 | +2.2784e+01 | 5.67e-01 | 3.70e+00 | 0.00e+00 |\n", 262 | "| 31 | 527 | 254 | +2.2784e+01 | 5.67e-02 | 3.70e+00 | 0.00e+00 |\n", 263 | "| 32 | 544 | 261 | +2.2716e+01 | 1.13e-01 | 3.83e+00 | 0.00e+00 |\n", 264 | "| 33 | 561 | 269 | +2.2716e+01 | 1.13e-02 | 3.83e+00 | 0.00e+00 |\n", 265 | "| 34 | 578 | 273 | +2.2685e+01 | 7.93e-02 | 3.79e+00 | 0.00e+00 |\n", 266 | "| 35 | 595 | 281 | +2.2685e+01 | 7.93e-03 | 3.79e+00 | 0.00e+00 |\n", 267 | "| 36 | 612 | 286 | +2.2663e+01 | 5.55e-02 | 3.77e+00 | 0.00e+00 |\n", 268 | "| 37 | 629 | 293 | +2.2663e+01 | 5.55e-03 | 3.77e+00 | 0.00e+00 |\n", 269 | "| 38 | 646 | 298 | +2.2647e+01 | 3.89e-02 | 3.74e+00 | 0.00e+00 |\n", 270 | "| 39 | 663 | 305 | +2.2647e+01 | 3.89e-03 | 3.74e+00 | 0.00e+00 |\n", 271 | "| 40 | 680 | 309 | +2.2634e+01 | 2.72e-02 | 3.72e+00 | 0.00e+00 |\n", 272 | "| 41 | 697 | 315 | +2.2634e+01 | 6.10e-03 | 3.72e+00 | 0.00e+00 |\n", 273 | "| 42 | 714 | 320 | +2.2617e+01 | 4.27e-02 | 3.71e+00 | 0.00e+00 |\n", 274 | "| 43 | 731 | 327 | +2.2617e+01 | 4.27e-03 | 3.71e+00 | 0.00e+00 |\n", 275 | "| 44 | 748 | 332 | +2.2603e+01 | 2.99e-02 | 3.68e+00 | 0.00e+00 |\n", 276 | "| 45 | 765 | 338 | +2.2547e+01 | 2.09e-01 | 3.68e+00 | 0.00e+00 |\n", 277 | "| 46 | 782 | 346 | +2.2547e+01 | 2.09e-02 | 3.68e+00 | 0.00e+00 |\n", 278 | "| 47 | 799 | 352 | +2.2507e+01 | 1.47e-01 | 3.65e+00 | 0.00e+00 |\n", 279 | "| 48 | 816 | 362 | +2.2294e+01 | 1.03e+00 | 3.60e+00 | 0.00e+00 |\n", 280 | "| 49 | 833 | 378 | +2.2294e+01 | 1.03e-01 | 3.60e+00 | 0.00e+00 |\n", 281 | "| 50 | 850 | 388 | +2.2172e+01 | 7.18e-01 | 3.80e+00 | 0.00e+00 |\n", 282 | "| 51 | 867 | 404 | +2.2172e+01 | 7.18e-02 | 3.80e+00 | 0.00e+00 |\n", 283 | "| 52 | 884 | 413 | +2.2172e+01 | 7.18e-03 | 3.80e+00 | 0.00e+00 |\n", 284 | "| 53 | 901 | 418 | +2.2158e+01 | 5.03e-02 | 3.81e+00 | 0.00e+00 |\n", 285 | "| 54 | 918 | 428 | +2.2098e+01 | 1.01e-01 | 3.90e+00 | 0.00e+00 |\n", 286 | "| 55 | 935 | 438 | +2.2098e+01 | 1.01e-02 | 3.90e+00 | 0.00e+00 |\n", 287 | "| 56 | 952 | 443 | +2.2081e+01 | 7.04e-02 | 3.94e+00 | 0.00e+00 |\n", 288 | "| 57 | 969 | 453 | +2.2081e+01 | 7.04e-03 | 3.94e+00 | 0.00e+00 |\n", 289 | "| 58 | 986 | 458 | +2.2067e+01 | 4.93e-02 | 3.98e+00 | 0.00e+00 |\n", 290 | "| 59 | 1003 | 469 | +2.2067e+01 | 2.46e-02 | 3.98e+00 | 0.00e+00 |\n", 291 | "| 60 | 1020 | 479 | +2.2016e+01 | 1.72e-01 | 4.23e+00 | 0.00e+00 |\n", 292 | "| 61 | 1037 | 490 | +2.2016e+01 | 1.72e-02 | 4.23e+00 | 0.00e+00 |\n", 293 | "| 62 | 1054 | 497 | +2.2016e+01 | 1.72e-03 | 4.23e+00 | 0.00e+00 |\n", 294 | "| 63 | 1071 | 500 | +2.2004e+01 | 1.21e-02 | 4.20e+00 | 0.00e+00 |\n", 295 | "| 64 | 1088 | 507 | +2.1953e+01 | 8.45e-02 | 3.84e+00 | 0.00e+00 |\n", 296 | "| 65 | 1105 | 517 | +2.1817e+01 | 1.69e-01 | 3.77e+00 | 0.00e+00 |\n", 297 | "| 66 | 1122 | 528 | +2.1817e+01 | 1.69e-02 | 3.77e+00 | 0.00e+00 |\n", 298 | "| 67 | 1139 | 535 | +2.1817e+01 | 1.69e-03 | 3.77e+00 | 0.00e+00 |\n", 299 | "| 68 | 1156 | 539 | +2.1807e+01 | 1.18e-02 | 3.72e+00 | 0.00e+00 |\n", 300 | "| 69 | 1173 | 544 | +2.1807e+01 | 1.18e-03 | 3.72e+00 | 0.00e+00 |\n", 301 | "| 70 | 1190 | 548 | +2.1801e+01 | 8.28e-03 | 3.73e+00 | 0.00e+00 |\n", 302 | "| 71 | 1207 | 553 | +2.1801e+01 | 8.28e-04 | 3.73e+00 | 0.00e+00 |\n", 303 | "| 72 | 1224 | 557 | +2.1796e+01 | 5.79e-03 | 3.73e+00 | 0.00e+00 |\n", 304 | "| 73 | 1241 | 563 | +2.1767e+01 | 4.06e-02 | 3.76e+00 | 0.00e+00 |\n", 305 | "| 74 | 1258 | 569 | +2.1767e+01 | 4.06e-03 | 3.76e+00 | 0.00e+00 |\n", 306 | "| 75 | 1275 | 575 | +2.1746e+01 | 2.84e-02 | 3.63e+00 | 0.00e+00 |\n", 307 | "| 76 | 1292 | 581 | +2.1746e+01 | 2.84e-03 | 3.63e+00 | 0.00e+00 |\n", 308 | "| 77 | 1309 | 586 | +2.1731e+01 | 1.99e-02 | 3.58e+00 | 0.00e+00 |\n", 309 | "| 78 | 1326 | 592 | +2.1731e+01 | 2.09e-03 | 3.58e+00 | 0.00e+00 |\n", 310 | "| 79 | 1343 | 597 | +2.1719e+01 | 1.46e-02 | 3.55e+00 | 0.00e+00 |\n", 311 | "| 80 | 1360 | 604 | +2.1654e+01 | 1.03e-01 | 3.20e+00 | 0.00e+00 |\n", 312 | "| 81 | 1377 | 612 | +2.1326e+01 | 7.18e-01 | 3.99e+00 | 0.00e+00 |\n", 313 | "| 82 | 1394 | 626 | +2.1326e+01 | 7.18e-02 | 3.99e+00 | 0.00e+00 |\n", 314 | "| 83 | 1411 | 635 | +2.1326e+01 | 7.18e-03 | 3.99e+00 | 0.00e+00 |\n", 315 | "| 84 | 1428 | 640 | +2.1294e+01 | 5.02e-02 | 3.15e+00 | 0.00e+00 |\n", 316 | "| 85 | 1445 | 650 | +2.1294e+01 | 5.02e-03 | 3.15e+00 | 0.00e+00 |\n", 317 | "| 86 | 1462 | 656 | +2.1280e+01 | 3.52e-02 | 3.17e+00 | 0.00e+00 |\n", 318 | "| 87 | 1479 | 664 | +2.1280e+01 | 3.52e-03 | 3.17e+00 | 0.00e+00 |\n", 319 | "| 88 | 1496 | 670 | +2.1269e+01 | 2.46e-02 | 3.21e+00 | 0.00e+00 |\n", 320 | "| 89 | 1513 | 678 | +2.1269e+01 | 2.46e-03 | 3.21e+00 | 0.00e+00 |\n", 321 | "| 90 | 1530 | 684 | +2.1262e+01 | 1.72e-02 | 3.24e+00 | 0.00e+00 |\n", 322 | "| 91 | 1547 | 691 | +2.1224e+01 | 1.21e-01 | 3.36e+00 | 0.00e+00 |\n", 323 | "| 92 | 1564 | 702 | +2.1224e+01 | 1.21e-02 | 3.36e+00 | 0.00e+00 |\n", 324 | "| 93 | 1581 | 710 | +2.1199e+01 | 8.44e-02 | 3.40e+00 | 0.00e+00 |\n", 325 | "| 94 | 1598 | 721 | +2.1199e+01 | 4.22e-02 | 3.40e+00 | 0.00e+00 |\n", 326 | "| 95 | 1615 | 730 | +2.1199e+01 | 4.22e-03 | 3.40e+00 | 0.00e+00 |\n", 327 | "| 96 | 1632 | 737 | +2.1188e+01 | 2.96e-02 | 3.41e+00 | 0.00e+00 |\n", 328 | "| 97 | 1649 | 747 | +2.1137e+01 | 2.07e-01 | 3.45e+00 | 0.00e+00 |\n", 329 | "| 98 | 1666 | 758 | +2.1137e+01 | 2.07e-02 | 3.45e+00 | 0.00e+00 |\n", 330 | "| 99 | 1683 | 767 | +2.1137e+01 | 2.86e-03 | 3.45e+00 | 0.00e+00 |\n", 331 | "| 100 | 1700 | 774 | +2.1128e+01 | 2.01e-02 | 3.45e+00 | 0.00e+00 |\n", 332 | "\n", 333 | "The maximum number of function evaluations is exceeded.\n", 334 | "Number of iterations: 100, function evaluations: 1700, CG iterations: 774, optimality: 3.45e+00, constraint violation: 0.00e+00, execution time: 4e+01 s.\n" 335 | ] 336 | } 337 | ] 338 | }, 339 | { 340 | "cell_type": "code", 341 | "source": [ 342 | "optimal_truss = generate_truss(results.x)\n", 343 | "optimal_truss.analyze()\n", 344 | "print(\n", 345 | " \"mass = {:.2f} kg; \".format(optimal_truss.mass),\n", 346 | " \"FOS = {:.2f}; \".format(optimal_truss.fos),\n", 347 | " \"deflection = {:.2f} cm; \".format(optimal_truss.deflection*100)\n", 348 | ")\n", 349 | "trussme.visualize.plot_truss(optimal_truss, starting_shape=\"force\");" 350 | ], 351 | "metadata": { 352 | "colab": { 353 | "base_uri": "https://localhost:8080/", 354 | "height": 423 355 | }, 356 | "id": "BQ5xCYibymLu", 357 | "outputId": "2c43ae69-7406-4582-d3d4-814ba6e2ee68" 358 | }, 359 | "execution_count": 8, 360 | "outputs": [ 361 | { 362 | "output_type": "stream", 363 | "name": "stdout", 364 | "text": [ 365 | "mass = 21.13 kg; FOS = 1.04; deflection = 0.58 cm; \n" 366 | ] 367 | }, 368 | { 369 | "output_type": "display_data", 370 | "data": { 371 | "text/plain": [ 372 | "
" 373 | ], 374 | "image/png": "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\n" 375 | }, 376 | "metadata": {} 377 | } 378 | ] 379 | }, 380 | { 381 | "cell_type": "markdown", 382 | "source": [ 383 | "# Mass Minimal by Joint Coordinates and Cross-Sections\n", 384 | "Next, let's generate a mass-minimal truss by moving the joints around and updating cross-section size" 385 | ], 386 | "metadata": { 387 | "id": "UOj8fnXsy3F9" 388 | } 389 | }, 390 | { 391 | "cell_type": "code", 392 | "source": [ 393 | "x0, objective, constraints, generate_truss, bounds = trussme.make_optimization_functions(truss_from_commands, goals, joint_coordinates=True, shape_parameters=True)" 394 | ], 395 | "metadata": { 396 | "id": "Lkd_mSJwy77y" 397 | }, 398 | "execution_count": 9, 399 | "outputs": [] 400 | }, 401 | { 402 | "cell_type": "code", 403 | "source": [ 404 | "results = scipy.optimize.minimize(\n", 405 | " objective,\n", 406 | " x0,\n", 407 | " constraints=scipy.optimize.NonlinearConstraint(constraints, -numpy.inf, 0.0, keep_feasible=True),\n", 408 | " bounds=scipy.optimize.Bounds(bounds[0], bounds[1], keep_feasible=True),\n", 409 | " method='trust-constr',\n", 410 | " options={\n", 411 | " \"verbose\": 2,\n", 412 | " \"maxiter\": 100,\n", 413 | " }\n", 414 | ")" 415 | ], 416 | "metadata": { 417 | "outputId": "08d8f5b6-c565-43e6-e3f4-9badefc269ea", 418 | "colab": { 419 | "base_uri": "https://localhost:8080/" 420 | }, 421 | "id": "Gs6RL4Xhy77z" 422 | }, 423 | "execution_count": 12, 424 | "outputs": [ 425 | { 426 | "output_type": "stream", 427 | "name": "stdout", 428 | "text": [ 429 | "| niter |f evals|CG iter| obj func |tr radius | opt | c viol |\n", 430 | "|-------|-------|-------|-------------|----------|----------|----------|\n", 431 | "| 1 | 36 | 0 | +3.7583e+01 | 1.00e+00 | 3.49e+00 | 0.00e+00 |\n", 432 | "| 2 | 72 | 1 | +2.1379e+01 | 2.00e+00 | 2.06e+00 | 0.00e+00 |\n", 433 | "| 3 | 108 | 11 | +2.1379e+01 | 2.00e-01 | 2.06e+00 | 0.00e+00 |\n", 434 | "| 4 | 144 | 13 | +1.9409e+01 | 1.40e+00 | 1.88e+00 | 0.00e+00 |\n", 435 | "| 5 | 180 | 23 | +1.9409e+01 | 1.40e-01 | 1.88e+00 | 0.00e+00 |\n", 436 | "| 6 | 216 | 26 | +1.8174e+01 | 9.80e-01 | 1.77e+00 | 0.00e+00 |\n", 437 | "| 7 | 252 | 32 | +1.8174e+01 | 9.80e-02 | 1.77e+00 | 0.00e+00 |\n", 438 | "| 8 | 288 | 35 | +1.7375e+01 | 6.86e-01 | 1.69e+00 | 0.00e+00 |\n", 439 | "| 9 | 324 | 38 | +1.7375e+01 | 6.86e-02 | 1.69e+00 | 0.00e+00 |\n", 440 | "| 10 | 360 | 41 | +1.6846e+01 | 4.80e-01 | 1.91e+00 | 0.00e+00 |\n", 441 | "| 11 | 396 | 45 | +1.6846e+01 | 4.80e-02 | 1.91e+00 | 0.00e+00 |\n", 442 | "| 12 | 432 | 48 | +1.6490e+01 | 3.36e-01 | 1.87e+00 | 0.00e+00 |\n", 443 | "| 13 | 468 | 52 | +1.6490e+01 | 3.36e-02 | 1.87e+00 | 0.00e+00 |\n", 444 | "| 14 | 504 | 55 | +1.6244e+01 | 2.35e-01 | 1.83e+00 | 0.00e+00 |\n", 445 | "| 15 | 540 | 59 | +1.4645e+01 | 1.65e+00 | 1.62e+00 | 0.00e+00 |\n", 446 | "| 16 | 576 | 75 | +1.4645e+01 | 1.65e-01 | 1.62e+00 | 0.00e+00 |\n", 447 | "| 17 | 612 | 79 | +1.3642e+01 | 1.15e+00 | 1.48e+00 | 0.00e+00 |\n", 448 | "| 18 | 648 | 98 | +1.3642e+01 | 1.15e-01 | 1.48e+00 | 0.00e+00 |\n", 449 | "| 19 | 684 | 102 | +1.3642e+01 | 1.15e-02 | 1.48e+00 | 0.00e+00 |\n", 450 | "| 20 | 720 | 105 | +1.3575e+01 | 8.07e-02 | 1.48e+00 | 0.00e+00 |\n", 451 | "| 21 | 756 | 109 | +1.3134e+01 | 5.65e-01 | 1.42e+00 | 0.00e+00 |\n", 452 | "| 22 | 792 | 120 | +1.3134e+01 | 5.65e-02 | 1.42e+00 | 0.00e+00 |\n", 453 | "| 23 | 828 | 125 | +1.3134e+01 | 5.65e-03 | 1.42e+00 | 0.00e+00 |\n", 454 | "| 24 | 864 | 127 | +1.3101e+01 | 3.95e-02 | 1.42e+00 | 0.00e+00 |\n", 455 | "| 25 | 900 | 130 | +1.2888e+01 | 7.91e-02 | 1.39e+00 | 0.00e+00 |\n", 456 | "| 26 | 936 | 135 | +1.2888e+01 | 7.91e-03 | 1.39e+00 | 0.00e+00 |\n", 457 | "| 27 | 972 | 137 | +1.2843e+01 | 5.54e-02 | 1.38e+00 | 0.00e+00 |\n", 458 | "| 28 | 1008 | 142 | +1.2843e+01 | 5.54e-03 | 1.38e+00 | 0.00e+00 |\n", 459 | "| 29 | 1044 | 144 | +1.2811e+01 | 3.88e-02 | 1.38e+00 | 0.00e+00 |\n", 460 | "| 30 | 1080 | 148 | +1.2811e+01 | 3.88e-03 | 1.38e+00 | 0.00e+00 |\n", 461 | "| 31 | 1116 | 150 | +1.2788e+01 | 2.71e-02 | 1.38e+00 | 0.00e+00 |\n", 462 | "| 32 | 1152 | 153 | +1.2788e+01 | 2.71e-03 | 1.38e+00 | 0.00e+00 |\n", 463 | "| 33 | 1188 | 155 | +1.2772e+01 | 1.90e-02 | 1.37e+00 | 0.00e+00 |\n", 464 | "| 34 | 1224 | 158 | +1.2772e+01 | 1.90e-03 | 1.37e+00 | 0.00e+00 |\n", 465 | "| 35 | 1260 | 159 | +1.2760e+01 | 1.33e-02 | 1.39e+00 | 0.00e+00 |\n", 466 | "| 36 | 1296 | 161 | +1.2760e+01 | 1.33e-03 | 1.39e+00 | 0.00e+00 |\n", 467 | "| 37 | 1332 | 162 | +1.2760e+01 | 6.65e-04 | 1.39e+00 | 0.00e+00 |\n", 468 | "| 38 | 1368 | 163 | +1.2760e+01 | 3.32e-04 | 1.39e+00 | 0.00e+00 |\n", 469 | "| 39 | 1404 | 164 | +1.2758e+01 | 6.65e-04 | 1.37e+00 | 0.00e+00 |\n", 470 | "| 40 | 1440 | 165 | +1.2758e+01 | 3.32e-04 | 1.37e+00 | 0.00e+00 |\n", 471 | "| 41 | 1476 | 166 | +1.2758e+01 | 1.66e-04 | 1.37e+00 | 0.00e+00 |\n", 472 | "| 42 | 1512 | 167 | +1.2757e+01 | 3.32e-04 | 1.39e+00 | 0.00e+00 |\n", 473 | "| 43 | 1548 | 168 | +1.2757e+01 | 1.66e-04 | 1.39e+00 | 0.00e+00 |\n", 474 | "| 44 | 1584 | 169 | +1.2757e+01 | 8.31e-05 | 1.39e+00 | 0.00e+00 |\n", 475 | "| 45 | 1620 | 170 | +1.2757e+01 | 1.66e-04 | 1.37e+00 | 0.00e+00 |\n", 476 | "| 46 | 1656 | 171 | +1.2757e+01 | 8.31e-05 | 1.37e+00 | 0.00e+00 |\n", 477 | "| 47 | 1692 | 172 | +1.2757e+01 | 4.15e-05 | 1.37e+00 | 0.00e+00 |\n", 478 | "| 48 | 1728 | 173 | +1.2757e+01 | 8.31e-05 | 1.38e+00 | 0.00e+00 |\n", 479 | "| 49 | 1764 | 174 | +1.2757e+01 | 4.15e-05 | 1.38e+00 | 0.00e+00 |\n", 480 | "| 50 | 1800 | 175 | +1.2757e+01 | 2.08e-05 | 1.38e+00 | 0.00e+00 |\n", 481 | "| 51 | 1836 | 176 | +1.2757e+01 | 4.15e-05 | 1.38e+00 | 0.00e+00 |\n", 482 | "| 52 | 1872 | 177 | +1.2757e+01 | 2.08e-05 | 1.38e+00 | 0.00e+00 |\n", 483 | "| 53 | 1908 | 178 | +1.2757e+01 | 1.04e-05 | 1.38e+00 | 0.00e+00 |\n", 484 | "| 54 | 1944 | 179 | +1.2756e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 485 | "| 55 | 1980 | 180 | +1.2756e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 486 | "| 56 | 2016 | 181 | +1.2756e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 487 | "| 57 | 2052 | 182 | +1.2756e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 488 | "| 58 | 2088 | 183 | +1.2756e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 489 | "| 59 | 2124 | 184 | +1.2756e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 490 | "| 60 | 2160 | 185 | +1.2756e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 491 | "| 61 | 2196 | 186 | +1.2756e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 492 | "| 62 | 2232 | 187 | +1.2756e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 493 | "| 63 | 2268 | 188 | +1.2756e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 494 | "| 64 | 2304 | 189 | +1.2756e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 495 | "| 65 | 2340 | 190 | +1.2756e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 496 | "| 66 | 2376 | 191 | +1.2756e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 497 | "| 67 | 2412 | 192 | +1.2756e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 498 | "| 68 | 2448 | 193 | +1.2756e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 499 | "| 69 | 2484 | 194 | +1.2756e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 500 | "| 70 | 2520 | 195 | +1.2755e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 501 | "| 71 | 2556 | 196 | +1.2755e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 502 | "| 72 | 2592 | 197 | +1.2755e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 503 | "| 73 | 2628 | 198 | +1.2755e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 504 | "| 74 | 2664 | 199 | +1.2755e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 505 | "| 75 | 2700 | 200 | +1.2755e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 506 | "| 76 | 2736 | 201 | +1.2755e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 507 | "| 77 | 2772 | 202 | +1.2755e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 508 | "| 78 | 2808 | 203 | +1.2755e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 509 | "| 79 | 2844 | 204 | +1.2755e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 510 | "| 80 | 2880 | 205 | +1.2755e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 511 | "| 81 | 2916 | 206 | +1.2755e+01 | 1.04e-05 | 1.37e+00 | 0.00e+00 |\n", 512 | "| 82 | 2952 | 207 | +1.2755e+01 | 1.04e-05 | 1.39e+00 | 0.00e+00 |\n", 513 | "| 83 | 2988 | 208 | +1.2755e+01 | 5.19e-06 | 1.39e+00 | 0.00e+00 |\n", 514 | "| 84 | 3024 | 209 | +1.2755e+01 | 1.04e-05 | 1.31e+00 | 0.00e+00 |\n", 515 | "| 85 | 3060 | 210 | +1.2755e+01 | 5.19e-06 | 1.31e+00 | 0.00e+00 |\n", 516 | "| 86 | 3096 | 211 | +1.2755e+01 | 2.60e-06 | 1.31e+00 | 0.00e+00 |\n", 517 | "| 87 | 3132 | 212 | +1.2755e+01 | 1.30e-06 | 1.31e+00 | 0.00e+00 |\n", 518 | "| 88 | 3168 | 213 | +1.2755e+01 | 6.49e-07 | 1.31e+00 | 0.00e+00 |\n", 519 | "| 89 | 3204 | 214 | +1.2755e+01 | 3.25e-07 | 1.31e+00 | 0.00e+00 |\n", 520 | "| 90 | 3240 | 215 | +1.2755e+01 | 1.62e-07 | 1.31e+00 | 0.00e+00 |\n", 521 | "| 91 | 3276 | 216 | +1.2755e+01 | 8.11e-08 | 1.31e+00 | 0.00e+00 |\n", 522 | "| 92 | 3312 | 217 | +1.2755e+01 | 4.06e-08 | 1.31e+00 | 0.00e+00 |\n", 523 | "| 93 | 3348 | 218 | +1.2755e+01 | 2.03e-08 | 1.31e+00 | 0.00e+00 |\n", 524 | "| 94 | 3384 | 219 | +1.2755e+01 | 1.01e-08 | 1.31e+00 | 0.00e+00 |\n", 525 | "| 95 | 3420 | 220 | +1.2755e+01 | 5.07e-09 | 1.31e+00 | 0.00e+00 |\n", 526 | "| 96 | 3456 | 220 | +1.2755e+01 | 1.00e+00 | 1.31e+00 | 0.00e+00 |\n", 527 | "| 97 | 3492 | 227 | +1.2755e+01 | 1.00e-01 | 1.31e+00 | 0.00e+00 |\n", 528 | "| 98 | 3528 | 230 | +1.2755e+01 | 1.00e-02 | 1.31e+00 | 0.00e+00 |\n", 529 | "| 99 | 3564 | 232 | +1.2695e+01 | 2.00e-02 | 1.39e+00 | 0.00e+00 |\n", 530 | "| 100 | 3600 | 234 | +1.2695e+01 | 2.00e-03 | 1.39e+00 | 0.00e+00 |\n", 531 | "\n", 532 | "The maximum number of function evaluations is exceeded.\n", 533 | "Number of iterations: 100, function evaluations: 3600, CG iterations: 234, optimality: 1.39e+00, constraint violation: 0.00e+00, execution time: 6.1e+01 s.\n" 534 | ] 535 | } 536 | ] 537 | }, 538 | { 539 | "cell_type": "code", 540 | "source": [ 541 | "optimal_truss = generate_truss(results.x)\n", 542 | "optimal_truss.analyze()\n", 543 | "print(\n", 544 | " \"mass = {:.2f} kg; \".format(optimal_truss.mass),\n", 545 | " \"FOS = {:.2f}; \".format(optimal_truss.fos),\n", 546 | " \"deflection = {:.2f} cm; \".format(optimal_truss.deflection*100)\n", 547 | ")\n", 548 | "trussme.visualize.plot_truss(optimal_truss, starting_shape=\"force\");" 549 | ], 550 | "metadata": { 551 | "outputId": "defc4ef0-f197-4bfc-be4d-800fe271b048", 552 | "colab": { 553 | "base_uri": "https://localhost:8080/", 554 | "height": 423 555 | }, 556 | "id": "nyiZH-rvy77z" 557 | }, 558 | "execution_count": 13, 559 | "outputs": [ 560 | { 561 | "output_type": "stream", 562 | "name": "stdout", 563 | "text": [ 564 | "mass = 12.69 kg; FOS = 1.00; deflection = 0.54 cm; \n" 565 | ] 566 | }, 567 | { 568 | "output_type": "display_data", 569 | "data": { 570 | "text/plain": [ 571 | "
" 572 | ], 573 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAgMAAAGFCAYAAABg2vAPAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA9g0lEQVR4nO3d6VdceX4f/vetvdh3ECAEQvu+gxCLWt1qtaRGah87zkl8ZuxkEnsWz4wf5a/4PchMPON4Zo5jT2LnnMSeWFJL6lZ3a0ACCbTvC2IRS7Hv1F517/f34AIFagkhVNTC9/06h9M0t6AuCOq+73f5fBQhhAARERFJyxDtEyAiIqLoYhggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmGASIiIskxDBAREUmOYYCIiEhyDANERESSYxggIiKSHMMAERGR5BgGiIiIJMcwQEREJDmG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\n" 574 | }, 575 | "metadata": {} 576 | } 577 | ] 578 | }, 579 | { 580 | "cell_type": "code", 581 | "source": [], 582 | "metadata": { 583 | "id": "-qhlC5BXy-Bv" 584 | }, 585 | "execution_count": 11, 586 | "outputs": [] 587 | } 588 | ], 589 | "metadata": { 590 | "colab": { 591 | "name": "scratchpad", 592 | "provenance": [], 593 | "include_colab_link": true 594 | }, 595 | "kernelspec": { 596 | "display_name": "Python 3", 597 | "name": "python3" 598 | } 599 | }, 600 | "nbformat": 4, 601 | "nbformat_minor": 0 602 | } --------------------------------------------------------------------------------