Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 22 additions & 23 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,34 @@ name: Python package

on:
push:
branches: [ master ]
branches: [master]
pull_request:
branches: [ master ]
branches: [master]

jobs:
build:

runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["^3.6", "^3.7", "^3.8", "^3.9", "^3.10"]
python-version: ['^3.9', '^3.10', '^3.11', '^3.12']

steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with unittest
run: |
python -m unittest discover -p '*_test.py'
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ __pycache__/

# truss structure solution
result.svg
result.txt
result.txt

node_modules/
.svelte-kit
9 changes: 6 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": true
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
}
}
},
"python.testing.unittestArgs": ["-v", "-s", ".", "-p", "*_test.py"],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true
}
3 changes: 3 additions & 0 deletions geom2d/vector.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ def __eq__(self, other):
def __str__(self):
return f"({self.u}, {self.v}) with norm {self.norm}"

def __repr__(self):
return str(self)

def to_formatted_str(self, decimals: int):
"""
Returns a string of the form: '(u, v) with norm N', where
Expand Down
2 changes: 1 addition & 1 deletion graphic/simulation/draw.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from functools import reduce
from tkinter import Canvas

from geom2d import Circle, Polygon, Segment, Rect, AffineTransform
from geom2d import AffineTransform, Circle, Polygon, Rect, Segment


class CanvasDrawing:
Expand Down
Empty file modified run_tests.sh
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion structures/model/bar.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from eqs import Matrix
from geom2d import Segment
from .node import StrNode
from structures.model.node import StrNode


class StrBar:
Expand Down
5 changes: 2 additions & 3 deletions structures/model/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
from eqs import Vector as EqVector
from eqs import cholesky_solve
from geom2d import Vector
from structures.model.bar import StrBar
from structures.model.node import StrNode
from structures.solution.bar import StrBarSolution
from structures.solution.node import StrNodeSolution
from structures.solution.structure import StructureSolution

from .bar import StrBar
from .node import StrNode


class Structure:
"""
Expand Down
8 changes: 8 additions & 0 deletions structures/solution/node.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ def is_constrained(self):
self.__original_node.dx_constrained or self.__original_node.dy_constrained
)

@property
def dx_constrained(self) -> bool:
return self.__original_node.dx_constrained

@property
def dy_constrained(self) -> bool:
return self.__original_node.dy_constrained

@property
def loads(self):
"""
Expand Down
10 changes: 8 additions & 2 deletions structures/solution/structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def bounds_rect(self, margin: float, scale=1):
d_pos = [node.displaced_pos_scaled(scale) for node in self.nodes]
return make_rect_containing_with_margin(d_pos, margin)

def reaction_for_node(self, node: StrNodeSolution):
def reaction_for_node(self, node: StrNodeSolution) -> Vector:
"""
Computes the external reaction force for a given node.

Expand All @@ -57,4 +57,10 @@ def reaction_for_node(self, node: StrNodeSolution):
if node.is_loaded:
forces.append(node.net_load.opposite())

return reduce(operator.add, forces)
net_force = reduce(operator.add, forces)

# The reaction can only have the components of the constrained directions
return Vector(
u=net_force.u if node.dx_constrained else 0.0,
v=net_force.v if node.dy_constrained else 0.0,
)
8 changes: 4 additions & 4 deletions structures/tests/str_parse_test.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import unittest

import pkg_resources as res
from pathlib import Path

from geom2d import Point, Vector
from structures.parse import parse_structure


class StructureParseTest(unittest.TestCase):
def setUp(self):
str_bytes = res.resource_string(__name__, "test_str.txt")
str_string = str_bytes.decode("utf-8")
file_path = Path(__file__).parent / "test_str.txt"
with open(file_path, "r", encoding="utf-8") as f:
str_string = f.read()
self.structure = parse_structure(str_string)

def test_parse_nodes_count(self):
Expand Down
2 changes: 1 addition & 1 deletion structures/tests/structure_solution_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import unittest
from unittest.mock import patch, Mock
from unittest.mock import Mock, patch

from geom2d import Point
from structures.solution.node import StrNodeSolution
Expand Down
163 changes: 39 additions & 124 deletions structures/tests/structure_test.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
import unittest
from unittest.mock import patch
from operator import attrgetter

from eqs import Matrix
from geom2d import Point, Vector
from eqs.vector import Vector as EqVector
from structures.model.node import StrNode
from structures.model.bar import StrBar
from structures.model.node import StrNode
from structures.model.structure import Structure


class StructureTest(unittest.TestCase):
def setUp(self):
section = 5
young = 10
load = Vector(500, -1000)
self.section = 5
self.young = 2e7
self.load = Vector(500, -1000)

self.n_1 = StrNode(1, Point(0, 0))
self.n_2 = StrNode(2, Point(0, 200))
self.n_3 = StrNode(3, Point(400, 200), [load])
self.b_12 = StrBar(1, self.n_1, self.n_2, section, young)
self.b_23 = StrBar(2, self.n_2, self.n_3, section, young)
self.b_13 = StrBar(3, self.n_1, self.n_3, section, young)
self.n_3 = StrNode(3, Point(400, 200), [self.load])
self.b_12 = StrBar(1, self.n_1, self.n_2, self.section, self.young)
self.b_23 = StrBar(2, self.n_2, self.n_3, self.section, self.young)
self.b_13 = StrBar(3, self.n_1, self.n_3, self.section, self.young)

self.structure = Structure(
[self.n_1, self.n_2, self.n_3], [self.b_12, self.b_23, self.b_13]
Expand All @@ -35,128 +33,45 @@ def test_bars_count(self):
def test_loads_count(self):
self.assertEqual(1, self.structure.loads_count)

@patch("structures.model.structure.cholesky_solve")
def test_assemble_system_matrix(self, cholesky_mock):
eal3 = 0.1118033989
c2_eal3 = 0.8 * eal3
s2_eal3 = 0.2 * eal3
cs_eal3 = 0.4 * eal3
expected_mat = Matrix(6, 6).set_data(
[
c2_eal3,
cs_eal3,
0,
0,
-c2_eal3,
-cs_eal3,
cs_eal3,
0.25 + s2_eal3,
0,
-0.25,
-cs_eal3,
-s2_eal3,
0,
0,
0.125,
0,
-0.125,
0,
0,
-0.25,
0,
0.25,
0,
0,
-c2_eal3,
-cs_eal3,
-0.125,
0,
0.125 + c2_eal3,
cs_eal3,
-cs_eal3,
-s2_eal3,
0,
0,
cs_eal3,
s2_eal3,
]
)

self.structure.solve_structure()
[actual_mat, _] = cholesky_mock.call_args[0]

cholesky_mock.assert_called_once()
self.assertEqual(expected_mat, actual_mat)

@patch("structures.model.structure.cholesky_solve")
def test_system_matrix_constraints(self, cholesky_mock):
def test_solve_displacements(self):
self._set_external_constraints()
solution = self.structure.solve_structure()

eal3 = 0.1118033989
c2_eal3 = 0.8 * eal3
s2_eal3 = 0.2 * eal3
cs_eal3 = 0.4 * eal3
expected_mat = Matrix(6, 6).set_data(
[
1,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
0,
0,
0.125 + c2_eal3,
cs_eal3,
0,
0,
0,
0,
cs_eal3,
s2_eal3,
]
)
# Make sure the nodes are ordered by their id
solution.nodes.sort(key=attrgetter("id"))
node_1, node_2, node_3 = solution.nodes

self.structure.solve_structure()
[actual_mat, _] = cholesky_mock.call_args[0]
# The first and second nodes are XY constrained. No displacement
self.assertEqual(node_1.original_pos, node_1.displaced_pos)
self.assertEqual(node_2.original_pos, node_2.displaced_pos)

cholesky_mock.assert_called_once()
self.assertEqual(expected_mat, actual_mat)
# The third node should be displaced in the {+X, -Y} direction
self.assertGreater(node_3.global_disp.u, 0.0)
self.assertLess(node_3.global_disp.v, 0.0)

@patch("structures.model.structure.cholesky_solve")
def test_assemble_system_vector(self, cholesky_mock):
expected_vec = EqVector(6).set_data([0, 0, 0, 0, 500, -1000])
def test_solve_reactions(self):
self._set_external_constraints()
solution = self.structure.solve_structure()

self.structure.solve_structure()
[_, actual_vec] = cholesky_mock.call_args[0]
# Make sure the nodes are ordered by their id
solution.nodes.sort(key=attrgetter("id"))
node_1, node_2, node_3 = solution.nodes

self.assertEqual(expected_vec, actual_vec)
node_1_reaction = solution.reaction_for_node(node_1)
self.assertAlmostEqual(2000, node_1_reaction.u, delta=0.75)
self.assertAlmostEqual(1000, node_1_reaction.v, delta=0.75)

def test_solve_displacements(self):
self._set_external_constraints()
node_2_reaction = solution.reaction_for_node(node_2)
self.assertAlmostEqual(-2500, node_2_reaction.u, delta=0.75)
self.assertAlmostEqual(0, node_2_reaction.v, delta=0.75)

solution = self.structure.solve_structure()
# TODO
node_3_reaction = solution.reaction_for_node(node_3)
self.assertEqual(Vector(0, 0), node_3_reaction)

# The sum of external forces and reactions should be 0 in X and Y
sum_forces = self.load + node_1_reaction + node_2_reaction + node_3_reaction
self.assertAlmostEqual(0.0, sum_forces.u, delta=1.0)
self.assertAlmostEqual(0.0, sum_forces.v, delta=1.0)

def _set_external_constraints(self):
self.n_1.dx_constrained = True
Expand Down