├── 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 | "
"
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 | }
--------------------------------------------------------------------------------