bolt-pattern-elastic-method 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bolt_pattern_elastic_method-1.0.0/LICENSE.txt +21 -0
- bolt_pattern_elastic_method-1.0.0/PKG-INFO +97 -0
- bolt_pattern_elastic_method-1.0.0/README.md +65 -0
- bolt_pattern_elastic_method-1.0.0/bolt_pattern_elastic_method/__init__.py +1 -0
- bolt_pattern_elastic_method-1.0.0/bolt_pattern_elastic_method/core.py +714 -0
- bolt_pattern_elastic_method-1.0.0/pyproject.toml +18 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 A-Thomas-eng
|
|
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.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bolt_pattern_elastic_method
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Bolt pattern force distribution analysis
|
|
5
|
+
Project-URL: Homepage, https://github.com/A-Thomas-eng/bolt_pattern_elastic_method
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 A-Thomas-eng
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE.txt
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: matplotlib
|
|
30
|
+
Requires-Dist: numpy
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
Implements the methodology from NASA RP-1228.
|
|
34
|
+
|
|
35
|
+
Given a bolt pattern (positions + areas) and applied loads at arbitrary
|
|
36
|
+
locations, this module computes the axial and shear force on each bolt.
|
|
37
|
+
|
|
38
|
+
Outputs verified against the calulator here:
|
|
39
|
+
https://mechanicalc.com/reference/bolt-pattern-force-distribution
|
|
40
|
+
|
|
41
|
+
Example usage:
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
import os
|
|
45
|
+
import bolt_pattern_elastic_method
|
|
46
|
+
|
|
47
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
48
|
+
|
|
49
|
+
positions_1 = [(-3.5, 15), (3.5, 15), (-3.5, -15), (3.5, -15)
|
|
50
|
+
, (10, -15), (18.82, -12.14), (24.27, -4.635)
|
|
51
|
+
, (24.27, 4.635), (18.82, 12.14), (10, 15)
|
|
52
|
+
, (-10, 15), (-18.82, 12.14), (-24.27, 4.635)
|
|
53
|
+
, (-24.27, -4.635), (-18.82, -12.14), (-10, -15)] #[x,y]
|
|
54
|
+
|
|
55
|
+
# ------------------------------------------------------------------
|
|
56
|
+
# Example 1: 4-bolt rectangular pattern, in-plane eccentric shear
|
|
57
|
+
# A 1000 N force applied in X at (y=5) – creates torsion about Z
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
print("\nEXAMPLE 1 – General 3-D loading")
|
|
60
|
+
save_path="bolt_pattern_ex1.png"
|
|
61
|
+
analysis_1 = BoltPatternAnalysis(
|
|
62
|
+
bolts=[Bolt(x, y) for (x, y) in positions_1],
|
|
63
|
+
loads=[AppliedLoad(Fx=1500.0, Fy=500.0, Fz=5000.0, z=15.0),
|
|
64
|
+
AppliedLoad(Mz=1000.0)],
|
|
65
|
+
)
|
|
66
|
+
results_1 = analysis_1.solve()
|
|
67
|
+
print_results(results_1, analysis_1)
|
|
68
|
+
plot_bolt_pattern_3d(
|
|
69
|
+
analysis_1, results_1,
|
|
70
|
+
title="Example 1 - General 3-D loading",
|
|
71
|
+
show=False,
|
|
72
|
+
save_path=os.path.join(HERE, "save_path"),
|
|
73
|
+
)
|
|
74
|
+
print(f" → saved {save_path}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------------
|
|
78
|
+
# Example 2: general 3-D loading using the convenience function
|
|
79
|
+
# ------------------------------------------------------------------
|
|
80
|
+
print("\nEXAMPLE 2 - General 3-D loading (convenience function)")
|
|
81
|
+
analysis_2 = BoltPatternAnalysis(
|
|
82
|
+
bolts=[Bolt(x, y, label=lbl) for (x, y), lbl in zip(
|
|
83
|
+
[(-3, -3), (3, -3), (3, 3), (-3, 3)], ["TL", "TR", "BR", "BL"]
|
|
84
|
+
)],
|
|
85
|
+
loads=[AppliedLoad(Fx=200, Fy=-150, Fz=300,
|
|
86
|
+
Mx=500, My=-400, Mz=1000,
|
|
87
|
+
x=1.0, y=2.0, z=50.0)],
|
|
88
|
+
)
|
|
89
|
+
results_2 = analysis_2.solve()
|
|
90
|
+
print_results(results_2, analysis_2)
|
|
91
|
+
plot_bolt_pattern_3d(
|
|
92
|
+
analysis_2, results_2,
|
|
93
|
+
title="Example 2 - General 3-D Loading",
|
|
94
|
+
show=False,
|
|
95
|
+
save_path=os.path.join(HERE, "bolt_pattern_ex2.png"),
|
|
96
|
+
)
|
|
97
|
+
print(" → saved bolt_pattern_ex2.png")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Implements the methodology from NASA RP-1228.
|
|
2
|
+
|
|
3
|
+
Given a bolt pattern (positions + areas) and applied loads at arbitrary
|
|
4
|
+
locations, this module computes the axial and shear force on each bolt.
|
|
5
|
+
|
|
6
|
+
Outputs verified against the calulator here:
|
|
7
|
+
https://mechanicalc.com/reference/bolt-pattern-force-distribution
|
|
8
|
+
|
|
9
|
+
Example usage:
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
import os
|
|
13
|
+
import bolt_pattern_elastic_method
|
|
14
|
+
|
|
15
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
16
|
+
|
|
17
|
+
positions_1 = [(-3.5, 15), (3.5, 15), (-3.5, -15), (3.5, -15)
|
|
18
|
+
, (10, -15), (18.82, -12.14), (24.27, -4.635)
|
|
19
|
+
, (24.27, 4.635), (18.82, 12.14), (10, 15)
|
|
20
|
+
, (-10, 15), (-18.82, 12.14), (-24.27, 4.635)
|
|
21
|
+
, (-24.27, -4.635), (-18.82, -12.14), (-10, -15)] #[x,y]
|
|
22
|
+
|
|
23
|
+
# ------------------------------------------------------------------
|
|
24
|
+
# Example 1: 4-bolt rectangular pattern, in-plane eccentric shear
|
|
25
|
+
# A 1000 N force applied in X at (y=5) – creates torsion about Z
|
|
26
|
+
# ------------------------------------------------------------------
|
|
27
|
+
print("\nEXAMPLE 1 – General 3-D loading")
|
|
28
|
+
save_path="bolt_pattern_ex1.png"
|
|
29
|
+
analysis_1 = BoltPatternAnalysis(
|
|
30
|
+
bolts=[Bolt(x, y) for (x, y) in positions_1],
|
|
31
|
+
loads=[AppliedLoad(Fx=1500.0, Fy=500.0, Fz=5000.0, z=15.0),
|
|
32
|
+
AppliedLoad(Mz=1000.0)],
|
|
33
|
+
)
|
|
34
|
+
results_1 = analysis_1.solve()
|
|
35
|
+
print_results(results_1, analysis_1)
|
|
36
|
+
plot_bolt_pattern_3d(
|
|
37
|
+
analysis_1, results_1,
|
|
38
|
+
title="Example 1 - General 3-D loading",
|
|
39
|
+
show=False,
|
|
40
|
+
save_path=os.path.join(HERE, "save_path"),
|
|
41
|
+
)
|
|
42
|
+
print(f" → saved {save_path}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ------------------------------------------------------------------
|
|
46
|
+
# Example 2: general 3-D loading using the convenience function
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
print("\nEXAMPLE 2 - General 3-D loading (convenience function)")
|
|
49
|
+
analysis_2 = BoltPatternAnalysis(
|
|
50
|
+
bolts=[Bolt(x, y, label=lbl) for (x, y), lbl in zip(
|
|
51
|
+
[(-3, -3), (3, -3), (3, 3), (-3, 3)], ["TL", "TR", "BR", "BL"]
|
|
52
|
+
)],
|
|
53
|
+
loads=[AppliedLoad(Fx=200, Fy=-150, Fz=300,
|
|
54
|
+
Mx=500, My=-400, Mz=1000,
|
|
55
|
+
x=1.0, y=2.0, z=50.0)],
|
|
56
|
+
)
|
|
57
|
+
results_2 = analysis_2.solve()
|
|
58
|
+
print_results(results_2, analysis_2)
|
|
59
|
+
plot_bolt_pattern_3d(
|
|
60
|
+
analysis_2, results_2,
|
|
61
|
+
title="Example 2 - General 3-D Loading",
|
|
62
|
+
show=False,
|
|
63
|
+
save_path=os.path.join(HERE, "bolt_pattern_ex2.png"),
|
|
64
|
+
)
|
|
65
|
+
print(" → saved bolt_pattern_ex2.png")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .core import Bolt, AppliedLoad, BoltResult, BoltPatternAnalysis, bolt_pattern_force_distribution, plot_bolt_pattern_3d, print_results
|
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bolt Pattern Force Distribution
|
|
3
|
+
================================
|
|
4
|
+
Implements the methodology from:
|
|
5
|
+
https://mechanicalc.com/reference/bolt-pattern-force-distribution
|
|
6
|
+
|
|
7
|
+
Given a bolt pattern (positions + areas) and applied loads at arbitrary
|
|
8
|
+
locations, this module computes the axial and shear force on each bolt.
|
|
9
|
+
|
|
10
|
+
Coordinate system
|
|
11
|
+
-----------------
|
|
12
|
+
X, Y - in-plane axes (the plane of the bolt pattern)
|
|
13
|
+
Z - out-of-plane axis (bolt axis direction)
|
|
14
|
+
|
|
15
|
+
Applied load / moment sign convention follows the right-hand rule.
|
|
16
|
+
|
|
17
|
+
Usage example at the bottom of this file.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Sequence
|
|
25
|
+
|
|
26
|
+
import matplotlib
|
|
27
|
+
import matplotlib.pyplot as plt
|
|
28
|
+
import matplotlib.patches as mpatches
|
|
29
|
+
import matplotlib.colors as mcolors
|
|
30
|
+
import numpy as np
|
|
31
|
+
from mpl_toolkits.mplot3d import Axes3D # noqa: F401 – registers 3-D projection
|
|
32
|
+
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Data structures
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
_bolt_counter = 0
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Bolt:
|
|
43
|
+
"""A single bolt in the pattern.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
x, y : float
|
|
48
|
+
In-plane position of the bolt.
|
|
49
|
+
area : float
|
|
50
|
+
Tensile stress area of the bolt (default 1.0 for equal-size bolts).
|
|
51
|
+
label : str
|
|
52
|
+
Human-readable name. Auto-assigned as B1, B2, ... if not provided.
|
|
53
|
+
"""
|
|
54
|
+
x: float
|
|
55
|
+
y: float
|
|
56
|
+
area: float = 1.0
|
|
57
|
+
label: str = ""
|
|
58
|
+
|
|
59
|
+
def __post_init__(self):
|
|
60
|
+
global _bolt_counter
|
|
61
|
+
if not self.label:
|
|
62
|
+
_bolt_counter += 1
|
|
63
|
+
self.label = f"B{_bolt_counter}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class AppliedLoad:
|
|
68
|
+
"""A force/moment applied at a specific point.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
Fx, Fy, Fz : float
|
|
73
|
+
Force components (Fz is axial / out-of-plane).
|
|
74
|
+
Mx, My, Mz : float
|
|
75
|
+
Moment components about each axis (right-hand rule).
|
|
76
|
+
x, y, z : float
|
|
77
|
+
Location at which the load is applied.
|
|
78
|
+
The moments are transferred to the pattern centroid automatically.
|
|
79
|
+
"""
|
|
80
|
+
Fx: float = 0.0
|
|
81
|
+
Fy: float = 0.0
|
|
82
|
+
Fz: float = 0.0
|
|
83
|
+
Mx: float = 0.0
|
|
84
|
+
My: float = 0.0
|
|
85
|
+
Mz: float = 0.0
|
|
86
|
+
x: float = 0.0
|
|
87
|
+
y: float = 0.0
|
|
88
|
+
z: float = 0.0
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class BoltResult:
|
|
93
|
+
"""Forces resolved onto a single bolt."""
|
|
94
|
+
bolt: Bolt
|
|
95
|
+
# Axial (Z-direction) components
|
|
96
|
+
Fz_direct: float = 0.0 # due to direct Fz at centroid
|
|
97
|
+
Fz_Mcx: float = 0.0 # due to centroidal moment about X
|
|
98
|
+
Fz_Mcy: float = 0.0 # due to centroidal moment about Y
|
|
99
|
+
# Shear components
|
|
100
|
+
Fxy_direct_x: float = 0.0 # direct Fx distributed by area
|
|
101
|
+
Fxy_direct_y: float = 0.0 # direct Fy distributed by area
|
|
102
|
+
Fxy_Mcz_x: float = 0.0 # torsional moment – X component
|
|
103
|
+
Fxy_Mcz_y: float = 0.0 # torsional moment – Y component
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def Fz_total(self) -> float:
|
|
107
|
+
"""Total axial force on this bolt."""
|
|
108
|
+
return self.Fz_direct + self.Fz_Mcx + self.Fz_Mcy
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def Fx_total(self) -> float:
|
|
112
|
+
return self.Fxy_direct_x + self.Fxy_Mcz_x
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def Fy_total(self) -> float:
|
|
116
|
+
return self.Fxy_direct_y + self.Fxy_Mcz_y
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def F_shear(self) -> float:
|
|
120
|
+
"""Resultant shear magnitude."""
|
|
121
|
+
return math.hypot(self.Fx_total, self.Fy_total)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def F_total(self) -> float:
|
|
125
|
+
"""Total resultant force magnitude."""
|
|
126
|
+
return math.sqrt(self.Fx_total**2 + self.Fy_total**2 + self.Fz_total**2)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# ---------------------------------------------------------------------------
|
|
130
|
+
# Core computation
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class BoltPatternAnalysis:
|
|
135
|
+
"""Analyse force distribution over a bolt pattern.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
bolts : list[Bolt]
|
|
140
|
+
The bolt pattern.
|
|
141
|
+
loads : list[AppliedLoad]
|
|
142
|
+
All applied loads (can be at arbitrary locations).
|
|
143
|
+
"""
|
|
144
|
+
bolts: list[Bolt]
|
|
145
|
+
loads: list[AppliedLoad] = field(default_factory=list)
|
|
146
|
+
|
|
147
|
+
# ---- Pattern properties (computed lazily) ----
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def total_area(self) -> float:
|
|
151
|
+
"""Sum of bolt areas."""
|
|
152
|
+
return sum(b.area for b in self.bolts)
|
|
153
|
+
|
|
154
|
+
@property
|
|
155
|
+
def centroid(self) -> tuple[float, float]:
|
|
156
|
+
"""(xc, yc) – area-weighted centroid of the bolt pattern."""
|
|
157
|
+
A = self.total_area
|
|
158
|
+
xc = sum(b.area * b.x for b in self.bolts) / A
|
|
159
|
+
yc = sum(b.area * b.y for b in self.bolts) / A
|
|
160
|
+
return xc, yc
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def Ic_x(self) -> float:
|
|
164
|
+
"""Second moment of area about the centroidal X-axis (bending about X)."""
|
|
165
|
+
xc, yc = self.centroid
|
|
166
|
+
return sum(b.area * (b.y - yc)**2 for b in self.bolts)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def Ic_y(self) -> float:
|
|
170
|
+
"""Second moment of area about the centroidal Y-axis (bending about Y)."""
|
|
171
|
+
xc, yc = self.centroid
|
|
172
|
+
return sum(b.area * (b.x - xc)**2 for b in self.bolts)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def Ic_p(self) -> float:
|
|
176
|
+
"""Polar moment of area about the centroid (torsion about Z)."""
|
|
177
|
+
xc, yc = self.centroid
|
|
178
|
+
return sum(b.area * ((b.x - xc)**2 + (b.y - yc)**2) for b in self.bolts)
|
|
179
|
+
|
|
180
|
+
# ---- Translate all loads to the centroid ----
|
|
181
|
+
|
|
182
|
+
def centroidal_loads(self) -> tuple[float, float, float, float, float, float]:
|
|
183
|
+
"""Return (Fc_x, Fc_y, Fc_z, Mc_x, Mc_y, Mc_z) at the pattern centroid.
|
|
184
|
+
|
|
185
|
+
Each applied load is moved to the centroid using the cross-product
|
|
186
|
+
transfer rule: M_new = M_applied + R × F
|
|
187
|
+
where R is the vector from the centroid to the load application point.
|
|
188
|
+
"""
|
|
189
|
+
xc, yc = self.centroid
|
|
190
|
+
|
|
191
|
+
Fc_x = Fc_y = Fc_z = 0.0
|
|
192
|
+
Mc_x = Mc_y = Mc_z = 0.0
|
|
193
|
+
|
|
194
|
+
for load in self.loads:
|
|
195
|
+
# Direct force sums
|
|
196
|
+
Fc_x += load.Fx
|
|
197
|
+
Fc_y += load.Fy
|
|
198
|
+
Fc_z += load.Fz
|
|
199
|
+
|
|
200
|
+
# Moment transfer: R = (load.x - xc, load.y - yc, load.z - 0)
|
|
201
|
+
rx = load.x - xc
|
|
202
|
+
ry = load.y - yc
|
|
203
|
+
rz = load.z # centroid is at z=0 by convention
|
|
204
|
+
|
|
205
|
+
# Cross product R × F (right-hand rule)
|
|
206
|
+
cross_x = ry * load.Fz - rz * load.Fy
|
|
207
|
+
cross_y = rz * load.Fx - rx * load.Fz
|
|
208
|
+
cross_z = rx * load.Fy - ry * load.Fx
|
|
209
|
+
|
|
210
|
+
Mc_x += load.Mx + cross_x
|
|
211
|
+
Mc_y += load.My + cross_y
|
|
212
|
+
Mc_z += load.Mz + cross_z
|
|
213
|
+
|
|
214
|
+
return Fc_x, Fc_y, Fc_z, Mc_x, Mc_y, Mc_z
|
|
215
|
+
|
|
216
|
+
# ---- Distribute to individual bolts ----
|
|
217
|
+
|
|
218
|
+
def solve(self) -> list[BoltResult]:
|
|
219
|
+
"""Compute and return the force on each bolt."""
|
|
220
|
+
xc, yc = self.centroid
|
|
221
|
+
A = self.total_area
|
|
222
|
+
Ic_x = self.Ic_x
|
|
223
|
+
Ic_y = self.Ic_y
|
|
224
|
+
Ic_p = self.Ic_p
|
|
225
|
+
|
|
226
|
+
Fc_x, Fc_y, Fc_z, Mc_x, Mc_y, Mc_z = self.centroidal_loads()
|
|
227
|
+
|
|
228
|
+
results = []
|
|
229
|
+
for b in self.bolts:
|
|
230
|
+
r = BoltResult(bolt=b)
|
|
231
|
+
|
|
232
|
+
# Distance of bolt from centroid
|
|
233
|
+
dx = b.x - xc # rc_y component (distance in X from centroid)
|
|
234
|
+
dy = b.y - yc # rc_x component (distance in Y from centroid)
|
|
235
|
+
|
|
236
|
+
# ------------------------------------------------------------------
|
|
237
|
+
# Axial forces (Z direction)
|
|
238
|
+
# ------------------------------------------------------------------
|
|
239
|
+
# 1. Direct Fz distributed proportional to bolt area
|
|
240
|
+
r.Fz_direct = (b.area / A) * Fc_z
|
|
241
|
+
|
|
242
|
+
# 2. Moment Mc_x about X-axis → tensile/compressive along Z
|
|
243
|
+
# F = (Mc_x * rc_x,i / Ic_x) * A_i
|
|
244
|
+
# rc_x,i = dy (distance from centroid in Y direction)
|
|
245
|
+
if abs(Ic_x) > 0:
|
|
246
|
+
r.Fz_Mcx = (Mc_x * dy / Ic_x) * b.area
|
|
247
|
+
else:
|
|
248
|
+
r.Fz_Mcx = 0.0
|
|
249
|
+
|
|
250
|
+
# 3. Moment Mc_y about Y-axis → tensile/compressive along Z
|
|
251
|
+
# rc_y,i = dx (distance from centroid in X direction)
|
|
252
|
+
# Note: sign convention – positive Mc_y acts in -Z for bolts at +X
|
|
253
|
+
if abs(Ic_y) > 0:
|
|
254
|
+
r.Fz_Mcy = -(Mc_y * dx / Ic_y) * b.area
|
|
255
|
+
else:
|
|
256
|
+
r.Fz_Mcy = 0.0
|
|
257
|
+
|
|
258
|
+
# ------------------------------------------------------------------
|
|
259
|
+
# Shear forces (X-Y plane)
|
|
260
|
+
# ------------------------------------------------------------------
|
|
261
|
+
# 1. Direct shear distributed proportional to bolt area
|
|
262
|
+
r.Fxy_direct_x = (b.area / A) * Fc_x
|
|
263
|
+
r.Fxy_direct_y = (b.area / A) * Fc_y
|
|
264
|
+
|
|
265
|
+
# 2. Torsional moment Mc_z – shear perpendicular to radius vector
|
|
266
|
+
# F_i = (Mc_z * r_i / Ic_p) * A_i (magnitude)
|
|
267
|
+
# Direction: perpendicular to (dx, dy), i.e. (-dy, dx) normalised
|
|
268
|
+
if abs(Ic_p) > 0:
|
|
269
|
+
scale = (Mc_z / Ic_p) * b.area
|
|
270
|
+
r.Fxy_Mcz_x = -scale * dy
|
|
271
|
+
r.Fxy_Mcz_y = scale * dx
|
|
272
|
+
else:
|
|
273
|
+
r.Fxy_Mcz_x = 0.0
|
|
274
|
+
r.Fxy_Mcz_y = 0.0
|
|
275
|
+
|
|
276
|
+
results.append(r)
|
|
277
|
+
|
|
278
|
+
return results
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
# Convenience function
|
|
283
|
+
# ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
def bolt_pattern_force_distribution(
|
|
286
|
+
bolt_positions: Sequence[tuple[float, float]],
|
|
287
|
+
applied_loads: Sequence[dict],
|
|
288
|
+
bolt_areas: Sequence[float] | None = None,
|
|
289
|
+
bolt_labels: Sequence[str] | None = None,
|
|
290
|
+
) -> list[BoltResult]:
|
|
291
|
+
"""High-level helper function.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
bolt_positions : list of (x, y)
|
|
296
|
+
In-plane coordinates of each bolt.
|
|
297
|
+
applied_loads : list of dicts
|
|
298
|
+
Each dict may contain keys: Fx, Fy, Fz, Mx, My, Mz, x, y, z.
|
|
299
|
+
Missing keys default to 0.
|
|
300
|
+
bolt_areas : list of float, optional
|
|
301
|
+
Tensile stress area of each bolt. Defaults to 1.0 for all bolts
|
|
302
|
+
(equal-size bolts, which simplifies area-weighted sums).
|
|
303
|
+
bolt_labels : list of str, optional
|
|
304
|
+
Human-readable names for the bolts.
|
|
305
|
+
|
|
306
|
+
Returns
|
|
307
|
+
-------
|
|
308
|
+
list[BoltResult]
|
|
309
|
+
One entry per bolt with decomposed and total forces.
|
|
310
|
+
"""
|
|
311
|
+
n = len(bolt_positions)
|
|
312
|
+
if bolt_areas is None:
|
|
313
|
+
bolt_areas = [1.0] * n
|
|
314
|
+
if bolt_labels is None:
|
|
315
|
+
bolt_labels = [f"Bolt {i+1}" for i in range(n)]
|
|
316
|
+
|
|
317
|
+
bolts = [
|
|
318
|
+
Bolt(x=pos[0], y=pos[1], area=a, label=lbl)
|
|
319
|
+
for pos, a, lbl in zip(bolt_positions, bolt_areas, bolt_labels)
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
loads = [
|
|
323
|
+
AppliedLoad(
|
|
324
|
+
Fx=ld.get("Fx", 0.0),
|
|
325
|
+
Fy=ld.get("Fy", 0.0),
|
|
326
|
+
Fz=ld.get("Fz", 0.0),
|
|
327
|
+
Mx=ld.get("Mx", 0.0),
|
|
328
|
+
My=ld.get("My", 0.0),
|
|
329
|
+
Mz=ld.get("Mz", 0.0),
|
|
330
|
+
x=ld.get("x", 0.0),
|
|
331
|
+
y=ld.get("y", 0.0),
|
|
332
|
+
z=ld.get("z", 0.0),
|
|
333
|
+
)
|
|
334
|
+
for ld in applied_loads
|
|
335
|
+
]
|
|
336
|
+
|
|
337
|
+
analysis = BoltPatternAnalysis(bolts=bolts, loads=loads)
|
|
338
|
+
return analysis.solve()
|
|
339
|
+
|
|
340
|
+
# ---------------------------------------------------------------------------
|
|
341
|
+
# 3-D Visualisation
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
def plot_bolt_pattern_3d(
|
|
345
|
+
analysis: BoltPatternAnalysis,
|
|
346
|
+
results: list[BoltResult],
|
|
347
|
+
title: str = "Bolt Pattern Force Distribution",
|
|
348
|
+
show: bool = True,
|
|
349
|
+
save_path: str | None = None,
|
|
350
|
+
) -> plt.Figure:
|
|
351
|
+
"""Render a 3-D visualisation of the bolt pattern and its force distribution.
|
|
352
|
+
|
|
353
|
+
The bolt pattern sits in the Z=0 plane. For each bolt three arrows are drawn:
|
|
354
|
+
|
|
355
|
+
* **Blue** - total shear force vector (in the X-Y plane, originating at bolt)
|
|
356
|
+
* **Red** - total axial force (along Z, upward = tension, downward = compression)
|
|
357
|
+
* **Green** - resultant total force vector
|
|
358
|
+
|
|
359
|
+
The centroid is marked with a cross, and each applied load is shown as a
|
|
360
|
+
magenta arrow originating from its application point.
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
analysis : BoltPatternAnalysis
|
|
365
|
+
The analysis object (used for centroid, centroidal loads, etc.).
|
|
366
|
+
results : list[BoltResult]
|
|
367
|
+
Per-bolt results from ``analysis.solve()``.
|
|
368
|
+
title : str
|
|
369
|
+
Figure title.
|
|
370
|
+
show : bool
|
|
371
|
+
If True, call ``plt.show()`` at the end.
|
|
372
|
+
save_path : str or None
|
|
373
|
+
If given, save the figure to this path before showing.
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
matplotlib.figure.Figure
|
|
378
|
+
"""
|
|
379
|
+
matplotlib.rcParams.update({
|
|
380
|
+
"font.family": "monospace",
|
|
381
|
+
"axes.facecolor": "#0d1117",
|
|
382
|
+
"figure.facecolor": "#0d1117",
|
|
383
|
+
"text.color": "#e6edf3",
|
|
384
|
+
"axes.labelcolor": "#e6edf3",
|
|
385
|
+
"xtick.color": "#8b949e",
|
|
386
|
+
"ytick.color": "#8b949e",
|
|
387
|
+
"grid.color": "#21262d",
|
|
388
|
+
"axes.edgecolor": "#30363d",
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
fig = plt.figure(figsize=(16, 9))
|
|
392
|
+
fig.patch.set_facecolor("#0d1117")
|
|
393
|
+
|
|
394
|
+
# ── Main 3-D axis ────────────────────────────────────────────────────────
|
|
395
|
+
ax3d = fig.add_axes([0.02, 0.08, 0.62, 0.88], projection="3d")
|
|
396
|
+
ax3d.set_facecolor("#0d1117")
|
|
397
|
+
for pane in (ax3d.xaxis.pane, ax3d.yaxis.pane, ax3d.zaxis.pane):
|
|
398
|
+
pane.fill = False
|
|
399
|
+
pane.set_edgecolor("#21262d")
|
|
400
|
+
|
|
401
|
+
xc, yc = analysis.centroid
|
|
402
|
+
Fc_x, Fc_y, Fc_z, Mc_x, Mc_y, Mc_z = analysis.centroidal_loads()
|
|
403
|
+
|
|
404
|
+
# Normalisation scale so arrows are legible regardless of unit magnitudes
|
|
405
|
+
all_magnitudes = [r.F_total for r in results if r.F_total > 0]
|
|
406
|
+
if not all_magnitudes:
|
|
407
|
+
all_magnitudes = [1.0]
|
|
408
|
+
max_mag = max(all_magnitudes)
|
|
409
|
+
|
|
410
|
+
bolt_xs = [b.bolt.x for b in results]
|
|
411
|
+
bolt_ys = [b.bolt.y for b in results]
|
|
412
|
+
span = max(
|
|
413
|
+
max(bolt_xs) - min(bolt_xs),
|
|
414
|
+
max(bolt_ys) - min(bolt_ys),
|
|
415
|
+
1e-6,
|
|
416
|
+
)
|
|
417
|
+
arrow_scale = span * 0.45 / max_mag # arrows reach ~45 % of the pattern span
|
|
418
|
+
|
|
419
|
+
# ── Colour map: map |F_total| → colour ───────────────────────────────────
|
|
420
|
+
cmap = plt.cm.plasma
|
|
421
|
+
norm = mcolors.Normalize(vmin=0, vmax=max_mag)
|
|
422
|
+
|
|
423
|
+
# ── Draw bolt plate (semi-transparent polygon in Z=0 plane) ──────────────
|
|
424
|
+
# convex hull of bolt positions
|
|
425
|
+
from functools import reduce as _reduce
|
|
426
|
+
import operator as _op
|
|
427
|
+
|
|
428
|
+
pad = span * 0.18
|
|
429
|
+
plate_xs = [min(bolt_xs) - pad, max(bolt_xs) + pad,
|
|
430
|
+
max(bolt_xs) + pad, min(bolt_xs) - pad]
|
|
431
|
+
plate_ys = [min(bolt_ys) - pad, min(bolt_ys) - pad,
|
|
432
|
+
max(bolt_ys) + pad, max(bolt_ys) + pad]
|
|
433
|
+
plate_zs = [0, 0, 0, 0]
|
|
434
|
+
verts = [list(zip(plate_xs, plate_ys, plate_zs))]
|
|
435
|
+
plate = Poly3DCollection(verts, alpha=0.12, facecolor="#58a6ff", edgecolor="#30363d", linewidth=0.8)
|
|
436
|
+
ax3d.add_collection3d(plate)
|
|
437
|
+
|
|
438
|
+
# ── Draw bolts as cylinders ───────────────────────────────────────────────
|
|
439
|
+
theta = np.linspace(0, 2 * np.pi, 32)
|
|
440
|
+
bolt_r = span * 0.04
|
|
441
|
+
|
|
442
|
+
for r in results:
|
|
443
|
+
bx, by = r.bolt.x, r.bolt.y
|
|
444
|
+
color = cmap(norm(r.F_total))
|
|
445
|
+
|
|
446
|
+
# Cylinder body
|
|
447
|
+
cyl_h = span * 0.08
|
|
448
|
+
cx = bx + bolt_r * np.cos(theta)
|
|
449
|
+
cy = by + bolt_r * np.sin(theta)
|
|
450
|
+
z_top = np.full_like(theta, cyl_h)
|
|
451
|
+
z_bot = np.full_like(theta, -cyl_h)
|
|
452
|
+
ax3d.plot_surface(
|
|
453
|
+
np.array([cx, cx]), np.array([cy, cy]),
|
|
454
|
+
np.array([z_bot, z_top]),
|
|
455
|
+
color=color, alpha=0.85, linewidth=0,
|
|
456
|
+
)
|
|
457
|
+
# Top cap
|
|
458
|
+
ax3d.plot_surface(
|
|
459
|
+
np.array([[bx + bolt_r * np.cos(t) for t in theta]]),
|
|
460
|
+
np.array([[by + bolt_r * np.sin(t) for t in theta]]),
|
|
461
|
+
np.full((1, 32), cyl_h),
|
|
462
|
+
color=color, alpha=0.95, linewidth=0,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# ── Shear arrow (in-plane) ─────────────────────────────────────────
|
|
466
|
+
fs = r.F_shear
|
|
467
|
+
if fs > 1e-9:
|
|
468
|
+
us = r.Fx_total / fs * fs * arrow_scale
|
|
469
|
+
vs = r.Fy_total / fs * fs * arrow_scale
|
|
470
|
+
ax3d.quiver(bx, by, cyl_h, us, vs, 0,
|
|
471
|
+
color="#388bfd", linewidth=1.8, arrow_length_ratio=0.25)
|
|
472
|
+
|
|
473
|
+
# ── Axial arrow (Z) ────────────────────────────────────────────────
|
|
474
|
+
fz = r.Fz_total
|
|
475
|
+
if abs(fz) > 1e-9:
|
|
476
|
+
wz = fz * arrow_scale
|
|
477
|
+
ax3d.quiver(bx, by, cyl_h, 0, 0, wz,
|
|
478
|
+
color="#f85149" if fz < 0 else "#3fb950",
|
|
479
|
+
linewidth=1.8, arrow_length_ratio=0.25)
|
|
480
|
+
|
|
481
|
+
# ── Resultant arrow ────────────────────────────────────────────────
|
|
482
|
+
ft = r.F_total
|
|
483
|
+
if ft > 1e-9:
|
|
484
|
+
ux = r.Fx_total / ft * ft * arrow_scale
|
|
485
|
+
uy = r.Fy_total / ft * ft * arrow_scale
|
|
486
|
+
uz = r.Fz_total / ft * ft * arrow_scale
|
|
487
|
+
ax3d.quiver(bx, by, cyl_h, ux, uy, uz,
|
|
488
|
+
color="#ffa657", linewidth=1.2,
|
|
489
|
+
arrow_length_ratio=0.2, linestyle="dashed", alpha=0.7)
|
|
490
|
+
|
|
491
|
+
# Label
|
|
492
|
+
lbl = r.bolt.label or f"({bx:.1f},{by:.1f})"
|
|
493
|
+
ax3d.text(bx, by, cyl_h + span * 0.06, lbl,
|
|
494
|
+
color="#e6edf3", fontsize=7, ha="center", va="bottom",
|
|
495
|
+
fontweight="bold")
|
|
496
|
+
|
|
497
|
+
# ── Centroid marker ───────────────────────────────────────────────────────
|
|
498
|
+
ax3d.scatter([xc], [yc], [0], color="#d2a8ff", s=80, zorder=10, marker="+")
|
|
499
|
+
ax3d.text(xc, yc, span * 0.05, "centroid",
|
|
500
|
+
color="#d2a8ff", fontsize=7, ha="center")
|
|
501
|
+
|
|
502
|
+
# ── Applied load arrows (magenta, from their 3-D application point) ──────
|
|
503
|
+
if analysis.loads:
|
|
504
|
+
app_mag = max(
|
|
505
|
+
math.sqrt(l.Fx**2 + l.Fy**2 + l.Fz**2) for l in analysis.loads
|
|
506
|
+
) or 1.0
|
|
507
|
+
app_scale = span * 0.35 / app_mag
|
|
508
|
+
for ld in analysis.loads:
|
|
509
|
+
fm = math.sqrt(ld.Fx**2 + ld.Fy**2 + ld.Fz**2)
|
|
510
|
+
if fm < 1e-9:
|
|
511
|
+
continue
|
|
512
|
+
ax3d.quiver(ld.x, ld.y, ld.z,
|
|
513
|
+
ld.Fx * app_scale, ld.Fy * app_scale, ld.Fz * app_scale,
|
|
514
|
+
color="#bc8cff", linewidth=2.2, arrow_length_ratio=0.2)
|
|
515
|
+
ax3d.scatter([ld.x], [ld.y], [ld.z], color="#bc8cff", s=40, marker="*")
|
|
516
|
+
|
|
517
|
+
ax3d.set_xlabel("X", labelpad=4, fontsize=9, color="#8b949e")
|
|
518
|
+
ax3d.set_ylabel("Y", labelpad=4, fontsize=9, color="#8b949e")
|
|
519
|
+
ax3d.set_zlabel("Z (axial)", labelpad=4, fontsize=9, color="#8b949e")
|
|
520
|
+
ax3d.set_title(title, color="#e6edf3", fontsize=12, pad=10, fontweight="bold")
|
|
521
|
+
ax3d.view_init(elev=28, azim=-55)
|
|
522
|
+
ax3d.tick_params(colors="#8b949e", labelsize=7)
|
|
523
|
+
|
|
524
|
+
# ── Colour bar ────────────────────────────────────────────────────────────
|
|
525
|
+
sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm)
|
|
526
|
+
sm.set_array([])
|
|
527
|
+
cbar_ax = fig.add_axes([0.65, 0.15, 0.015, 0.65])
|
|
528
|
+
cbar = fig.colorbar(sm, cax=cbar_ax)
|
|
529
|
+
cbar.set_label("|F_total| per bolt", color="#e6edf3", fontsize=9)
|
|
530
|
+
cbar.ax.yaxis.set_tick_params(color="#8b949e", labelsize=7)
|
|
531
|
+
plt.setp(cbar.ax.yaxis.get_ticklabels(), color="#e6edf3")
|
|
532
|
+
cbar.outline.set_edgecolor("#30363d")
|
|
533
|
+
|
|
534
|
+
# ── Bar chart panel (right) ───────────────────────────────────────────────
|
|
535
|
+
ax_bar = fig.add_axes([0.70, 0.55, 0.28, 0.38])
|
|
536
|
+
ax_bar.set_facecolor("#161b22")
|
|
537
|
+
labels = [r.bolt.label or f"B{i+1}" for i, r in enumerate(results)]
|
|
538
|
+
fz_vals = [r.Fz_total for r in results]
|
|
539
|
+
fsh_vals = [r.F_shear for r in results]
|
|
540
|
+
ft_vals = [r.F_total for r in results]
|
|
541
|
+
x_idx = np.arange(len(results))
|
|
542
|
+
w = 0.26
|
|
543
|
+
b1 = ax_bar.bar(x_idx - w, fz_vals, width=w, label="Fz (axial)", color="#3fb950", alpha=0.85)
|
|
544
|
+
b2 = ax_bar.bar(x_idx, fsh_vals, width=w, label="|F_shear|", color="#388bfd", alpha=0.85)
|
|
545
|
+
b3 = ax_bar.bar(x_idx + w, ft_vals, width=w, label="|F_total|", color="#ffa657", alpha=0.85)
|
|
546
|
+
ax_bar.set_xticks(x_idx)
|
|
547
|
+
ax_bar.set_xticklabels(labels, fontsize=7, color="#e6edf3")
|
|
548
|
+
ax_bar.set_ylabel("Force", fontsize=8, color="#8b949e")
|
|
549
|
+
ax_bar.set_title("Per-bolt forces", fontsize=9, color="#e6edf3", fontweight="bold")
|
|
550
|
+
ax_bar.tick_params(colors="#8b949e", labelsize=7)
|
|
551
|
+
ax_bar.spines[:].set_edgecolor("#30363d")
|
|
552
|
+
ax_bar.legend(fontsize=7, facecolor="#0d1117", edgecolor="#30363d",
|
|
553
|
+
labelcolor="#e6edf3", loc="upper left")
|
|
554
|
+
ax_bar.axhline(0, color="#30363d", linewidth=0.8)
|
|
555
|
+
|
|
556
|
+
# ── Centroidal loads table ────────────────────────────────────────────────
|
|
557
|
+
ax_tbl = fig.add_axes([0.70, 0.08, 0.28, 0.40])
|
|
558
|
+
ax_tbl.set_facecolor("#161b22")
|
|
559
|
+
ax_tbl.axis("off")
|
|
560
|
+
col_labels = ["Bolt", "Fz", "Fx", "Fy", "|Fsh|", "|Ft|"]
|
|
561
|
+
rows = [
|
|
562
|
+
[
|
|
563
|
+
r.bolt.label or f"B{i+1}",
|
|
564
|
+
f"{r.Fz_total:+.1f}",
|
|
565
|
+
f"{r.Fx_total:+.1f}",
|
|
566
|
+
f"{r.Fy_total:+.1f}",
|
|
567
|
+
f"{r.F_shear:.1f}",
|
|
568
|
+
f"{r.F_total:.1f}",
|
|
569
|
+
]
|
|
570
|
+
for i, r in enumerate(results)
|
|
571
|
+
]
|
|
572
|
+
tbl = ax_tbl.table(
|
|
573
|
+
cellText=rows,
|
|
574
|
+
colLabels=col_labels,
|
|
575
|
+
loc="center",
|
|
576
|
+
cellLoc="center",
|
|
577
|
+
)
|
|
578
|
+
tbl.auto_set_font_size(False)
|
|
579
|
+
tbl.set_fontsize(7.5)
|
|
580
|
+
for (row, col), cell in tbl.get_celld().items():
|
|
581
|
+
cell.set_facecolor("#0d1117" if row % 2 == 0 else "#161b22")
|
|
582
|
+
cell.set_edgecolor("#30363d")
|
|
583
|
+
cell.set_text_props(color="#e6edf3")
|
|
584
|
+
if row == 0:
|
|
585
|
+
cell.set_facecolor("#21262d")
|
|
586
|
+
cell.set_text_props(color="#79c0ff", fontweight="bold")
|
|
587
|
+
ax_tbl.set_title(
|
|
588
|
+
f"Centroid ({xc:.2f}, {yc:.2f}) "
|
|
589
|
+
f"Fc=({Fc_x:.1f}, {Fc_y:.1f}, {Fc_z:.1f}) "
|
|
590
|
+
f"Mc=({Mc_x:.1f}, {Mc_y:.1f}, {Mc_z:.1f})",
|
|
591
|
+
fontsize=7, color="#8b949e", pad=4,
|
|
592
|
+
)
|
|
593
|
+
|
|
594
|
+
# ── Legend for arrows ─────────────────────────────────────────────────────
|
|
595
|
+
legend_elements = [
|
|
596
|
+
mpatches.Patch(color="#388bfd", label="Shear (in-plane)"),
|
|
597
|
+
mpatches.Patch(color="#3fb950", label="Axial +Z (tension)"),
|
|
598
|
+
mpatches.Patch(color="#f85149", label="Axial −Z (compression)"),
|
|
599
|
+
mpatches.Patch(color="#ffa657", label="Resultant"),
|
|
600
|
+
mpatches.Patch(color="#bc8cff", label="Applied load"),
|
|
601
|
+
]
|
|
602
|
+
fig.legend(
|
|
603
|
+
handles=legend_elements,
|
|
604
|
+
loc="lower center", ncol=5,
|
|
605
|
+
facecolor="#161b22", edgecolor="#30363d",
|
|
606
|
+
labelcolor="#e6edf3", fontsize=8,
|
|
607
|
+
bbox_to_anchor=(0.35, 0.01),
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
plt.suptitle(title, color="#e6edf3", fontsize=13, fontweight="bold", y=0.99)
|
|
611
|
+
|
|
612
|
+
if save_path:
|
|
613
|
+
fig.savefig(save_path, dpi=150, bbox_inches="tight", facecolor=fig.get_facecolor())
|
|
614
|
+
if show:
|
|
615
|
+
plt.show()
|
|
616
|
+
|
|
617
|
+
return fig
|
|
618
|
+
|
|
619
|
+
def print_results(results: list[BoltResult], analysis: BoltPatternAnalysis) -> None:
|
|
620
|
+
xc, yc = analysis.centroid
|
|
621
|
+
Fc_x, Fc_y, Fc_z, Mc_x, Mc_y, Mc_z = analysis.centroidal_loads()
|
|
622
|
+
|
|
623
|
+
print("=" * 60)
|
|
624
|
+
print("BOLT PATTERN PROPERTIES")
|
|
625
|
+
print(f" Total area A = {analysis.total_area:.4f}")
|
|
626
|
+
print(f" Centroid = ({xc:.4f}, {yc:.4f})")
|
|
627
|
+
print(f" Ic_x = {analysis.Ic_x:.4f}")
|
|
628
|
+
print(f" Ic_y = {analysis.Ic_y:.4f}")
|
|
629
|
+
print(f" Ic_p (polar) = {analysis.Ic_p:.4f}")
|
|
630
|
+
print()
|
|
631
|
+
print("CENTROIDAL LOADS")
|
|
632
|
+
print(f" Fc_x={Fc_x:.3f} Fc_y={Fc_y:.3f} Fc_z={Fc_z:.3f}")
|
|
633
|
+
print(f" Mc_x={Mc_x:.3f} Mc_y={Mc_y:.3f} Mc_z={Mc_z:.3f}")
|
|
634
|
+
print()
|
|
635
|
+
print(f"SUMMARY:")
|
|
636
|
+
print(f"Max fastener tensile load: {max(r.Fz_total for r in results):.1f}.")
|
|
637
|
+
print(f"Max fastener shear load: {max(r.F_shear for r in results):.1f}.")
|
|
638
|
+
print()
|
|
639
|
+
print("BOLT FORCES")
|
|
640
|
+
header = f"{'Bolt':<10} {'Fz_total':>10} {'Fx_total':>10} {'Fy_total':>10} {'F_shear':>10} {'F_total':>10}"
|
|
641
|
+
print(header)
|
|
642
|
+
print("-" * 60)
|
|
643
|
+
for r in results:
|
|
644
|
+
label = r.bolt.label or f"({r.bolt.x},{r.bolt.y})"
|
|
645
|
+
print(
|
|
646
|
+
f"{label:<10} "
|
|
647
|
+
f"{r.Fz_total:>10.4f} "
|
|
648
|
+
f"{r.Fx_total:>10.4f} "
|
|
649
|
+
f"{r.Fy_total:>10.4f} "
|
|
650
|
+
f"{r.F_shear:>10.4f} "
|
|
651
|
+
f"{r.F_total:>10.4f}"
|
|
652
|
+
)
|
|
653
|
+
print("=" * 60)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# ---------------------------------------------------------------------------
|
|
657
|
+
# Demo / self-test
|
|
658
|
+
# ---------------------------------------------------------------------------
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
# if __name__ == "__main__":
|
|
663
|
+
# import os
|
|
664
|
+
# HERE = os.path.dirname(os.path.abspath(__file__))
|
|
665
|
+
|
|
666
|
+
# positions_1 = [(-3.5, 15), (3.5, 15), (-3.5, -15), (3.5, -15)
|
|
667
|
+
# , (10, -15), (18.82, -12.14), (24.27, -4.635)
|
|
668
|
+
# , (24.27, 4.635), (18.82, 12.14), (10, 15)
|
|
669
|
+
# , (-10, 15), (-18.82, 12.14), (-24.27, 4.635)
|
|
670
|
+
# , (-24.27, -4.635), (-18.82, -12.14), (-10, -15)] #[x,y]
|
|
671
|
+
|
|
672
|
+
# # ------------------------------------------------------------------
|
|
673
|
+
# # Example 1: 4-bolt rectangular pattern, in-plane eccentric shear
|
|
674
|
+
# # A 1000 N force applied in X at (y=5) – creates torsion about Z
|
|
675
|
+
# # ------------------------------------------------------------------
|
|
676
|
+
# print("\nEXAMPLE 1 – General 3-D loading")
|
|
677
|
+
# save_path="bolt_pattern_ex1.png"
|
|
678
|
+
# analysis_1 = BoltPatternAnalysis(
|
|
679
|
+
# bolts=[Bolt(x, y) for (x, y) in positions_1],
|
|
680
|
+
# loads=[AppliedLoad(Fx=1500.0, Fy=500.0, Fz=5000.0, z=15.0),
|
|
681
|
+
# AppliedLoad(Mz=1000.0)],
|
|
682
|
+
# )
|
|
683
|
+
# results_1 = analysis_1.solve()
|
|
684
|
+
# print_results(results_1, analysis_1)
|
|
685
|
+
# plot_bolt_pattern_3d(
|
|
686
|
+
# analysis_1, results_1,
|
|
687
|
+
# title="Example 1 - General 3-D loading",
|
|
688
|
+
# show=False,
|
|
689
|
+
# save_path=os.path.join(HERE, "save_path"),
|
|
690
|
+
# )
|
|
691
|
+
# print(f" → saved {save_path}")
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
# # ------------------------------------------------------------------
|
|
695
|
+
# # Example 2: general 3-D loading using the convenience function
|
|
696
|
+
# # ------------------------------------------------------------------
|
|
697
|
+
# print("\nEXAMPLE 2 - General 3-D loading (convenience function)")
|
|
698
|
+
# analysis_2 = BoltPatternAnalysis(
|
|
699
|
+
# bolts=[Bolt(x, y, label=lbl) for (x, y), lbl in zip(
|
|
700
|
+
# [(-3, -3), (3, -3), (3, 3), (-3, 3)], ["TL", "TR", "BR", "BL"]
|
|
701
|
+
# )],
|
|
702
|
+
# loads=[AppliedLoad(Fx=200, Fy=-150, Fz=300,
|
|
703
|
+
# Mx=500, My=-400, Mz=1000,
|
|
704
|
+
# x=1.0, y=2.0, z=50.0)],
|
|
705
|
+
# )
|
|
706
|
+
# results_2 = analysis_2.solve()
|
|
707
|
+
# print_results(results_2, analysis_2)
|
|
708
|
+
# plot_bolt_pattern_3d(
|
|
709
|
+
# analysis_2, results_2,
|
|
710
|
+
# title="Example 2 - General 3-D Loading",
|
|
711
|
+
# show=False,
|
|
712
|
+
# save_path=os.path.join(HERE, "bolt_pattern_ex2.png"),
|
|
713
|
+
# )
|
|
714
|
+
# print(" → saved bolt_pattern_ex2.png")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bolt_pattern_elastic_method"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Bolt pattern force distribution analysis"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE.txt" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"matplotlib",
|
|
14
|
+
"numpy",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/A-Thomas-eng/bolt_pattern_elastic_method"
|