truss-analysis 2.0.9__py3-none-any.whl
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.
- truss_analysis/__init__.py +19 -0
- truss_analysis/assembly.py +93 -0
- truss_analysis/exceptions.py +25 -0
- truss_analysis/fileio.py +45 -0
- truss_analysis/i18n.py +30 -0
- truss_analysis/logger.py +12 -0
- truss_analysis/main.py +271 -0
- truss_analysis/model.py +94 -0
- truss_analysis/plotter.py +156 -0
- truss_analysis/postprocess.py +254 -0
- truss_analysis/report.py +120 -0
- truss_analysis/solver.py +87 -0
- truss_analysis/units.py +53 -0
- truss_analysis-2.0.9.dist-info/METADATA +449 -0
- truss_analysis-2.0.9.dist-info/RECORD +21 -0
- truss_analysis-2.0.9.dist-info/WHEEL +5 -0
- truss_analysis-2.0.9.dist-info/licenses/LICENSE +21 -0
- truss_analysis-2.0.9.dist-info/top_level.txt +2 -0
- utils/__init__.py +0 -0
- utils/data_contract.py +30 -0
- utils/font_loader.py +33 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
__version__ = "2.0.9"
|
|
2
|
+
|
|
3
|
+
from .assembly import assemble_global_matrices
|
|
4
|
+
from .model import Element, Node
|
|
5
|
+
from .postprocess import calculate_element_forces
|
|
6
|
+
from .solver import check_energy, solve
|
|
7
|
+
|
|
8
|
+
solve_truss = solve
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"__version__",
|
|
12
|
+
"Element",
|
|
13
|
+
"Node",
|
|
14
|
+
"solve",
|
|
15
|
+
"solve_truss",
|
|
16
|
+
"check_energy",
|
|
17
|
+
"assemble_global_matrices",
|
|
18
|
+
"calculate_element_forces",
|
|
19
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Assembly: global stiffness matrix and force vectors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .exceptions import AssemblyError
|
|
8
|
+
from .model import Element, Node
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def assemble_global_matrices(
|
|
12
|
+
nodes: list[Node],
|
|
13
|
+
elements: list[Element],
|
|
14
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray, list[int]]:
|
|
15
|
+
"""Assemble global stiffness matrix and force vectors.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
K: Global stiffness matrix
|
|
19
|
+
F_ext: External force vector (mechanical + thermal)
|
|
20
|
+
F_mechanical: Mechanical force vector only
|
|
21
|
+
fixed_dofs: List of fixed DOF indices
|
|
22
|
+
"""
|
|
23
|
+
n = len(nodes)
|
|
24
|
+
K = np.zeros((2 * n, 2 * n))
|
|
25
|
+
F_ext = np.zeros(2 * n)
|
|
26
|
+
F_mechanical = np.zeros(2 * n)
|
|
27
|
+
fixed_dofs = []
|
|
28
|
+
|
|
29
|
+
# Build node index map
|
|
30
|
+
node_map = {node.id: i for i, node in enumerate(nodes)}
|
|
31
|
+
|
|
32
|
+
# Assemble element contributions
|
|
33
|
+
for elem in elements:
|
|
34
|
+
if elem.node_i not in node_map or elem.node_j not in node_map:
|
|
35
|
+
raise AssemblyError(f"Element {elem.id} references non-existent nodes")
|
|
36
|
+
|
|
37
|
+
i = node_map[elem.node_i]
|
|
38
|
+
j = node_map[elem.node_j]
|
|
39
|
+
|
|
40
|
+
# Element geometry
|
|
41
|
+
dx = nodes[j].x - nodes[i].x
|
|
42
|
+
dy = nodes[j].y - nodes[i].y
|
|
43
|
+
L = np.sqrt(dx**2 + dy**2)
|
|
44
|
+
|
|
45
|
+
if L < 1e-12:
|
|
46
|
+
raise AssemblyError(f"Element {elem.id} has zero or negative length")
|
|
47
|
+
|
|
48
|
+
c = dx / L
|
|
49
|
+
s = dy / L
|
|
50
|
+
|
|
51
|
+
# Element stiffness matrix (local to global transformation)
|
|
52
|
+
k = elem.E * elem.A / L
|
|
53
|
+
ke = k * np.array(
|
|
54
|
+
[
|
|
55
|
+
[c**2, c * s, -(c**2), -c * s],
|
|
56
|
+
[c * s, s**2, -c * s, -(s**2)],
|
|
57
|
+
[-(c**2), -c * s, c**2, c * s],
|
|
58
|
+
[-c * s, -(s**2), c * s, s**2],
|
|
59
|
+
]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# DOF indices
|
|
63
|
+
dofs = [2 * i, 2 * i + 1, 2 * j, 2 * j + 1]
|
|
64
|
+
|
|
65
|
+
# Assemble into global matrix
|
|
66
|
+
for ii in range(4):
|
|
67
|
+
for jj in range(4):
|
|
68
|
+
K[dofs[ii], dofs[jj]] += ke[ii, jj]
|
|
69
|
+
|
|
70
|
+
# Thermal/fabrication forces
|
|
71
|
+
if elem.alpha != 0 or elem.delta_L_free != 0:
|
|
72
|
+
delta_L_thermal = elem.alpha * elem.delta_T * L
|
|
73
|
+
delta_L_prestress = delta_L_thermal + elem.delta_L_free
|
|
74
|
+
|
|
75
|
+
# Equivalent nodal forces (in global coordinates)
|
|
76
|
+
F_thermal = k * delta_L_prestress
|
|
77
|
+
F_ext[2 * i] -= F_thermal * c
|
|
78
|
+
F_ext[2 * i + 1] -= F_thermal * s
|
|
79
|
+
F_ext[2 * j] += F_thermal * c
|
|
80
|
+
F_ext[2 * j + 1] += F_thermal * s
|
|
81
|
+
|
|
82
|
+
# F_mechanical does NOT include thermal forces
|
|
83
|
+
# (they are internal, not external)
|
|
84
|
+
|
|
85
|
+
# Apply boundary conditions
|
|
86
|
+
for i, node in enumerate(nodes):
|
|
87
|
+
if node.is_support:
|
|
88
|
+
if node.support_dx:
|
|
89
|
+
fixed_dofs.append(2 * i)
|
|
90
|
+
if node.support_dy:
|
|
91
|
+
fixed_dofs.append(2 * i + 1)
|
|
92
|
+
|
|
93
|
+
return K, F_ext, F_mechanical, fixed_dofs
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TrussError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AssemblyError(TrussError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InputValidationError(TrussError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EnergyValidationError(TrussError):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SingularMatrixError(TrussError):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UnitConversionError(TrussError):
|
|
25
|
+
pass
|
truss_analysis/fileio.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from .exceptions import InputValidationError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_json(filepath):
|
|
10
|
+
if not os.path.exists(filepath):
|
|
11
|
+
raise FileNotFoundError(f"File not found: {filepath}")
|
|
12
|
+
size = os.path.getsize(filepath)
|
|
13
|
+
if size > 10 * 1024 * 1024:
|
|
14
|
+
raise InputValidationError(f"File too large: {size} bytes.")
|
|
15
|
+
try:
|
|
16
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
17
|
+
data = json.load(f)
|
|
18
|
+
except json.JSONDecodeError:
|
|
19
|
+
raise InputValidationError("Invalid JSON format.")
|
|
20
|
+
|
|
21
|
+
for key in ["nodes", "elements"]:
|
|
22
|
+
if key not in data:
|
|
23
|
+
raise InputValidationError(f"Missing key: '{key}'")
|
|
24
|
+
if not isinstance(data[key], list):
|
|
25
|
+
raise InputValidationError(f"'{key}' must be a list.")
|
|
26
|
+
|
|
27
|
+
node_ids = {n.get("id") for n in data["nodes"]}
|
|
28
|
+
for elem in data["elements"]:
|
|
29
|
+
if elem.get("node_i") not in node_ids or elem.get("node_j") not in node_ids:
|
|
30
|
+
raise InputValidationError(
|
|
31
|
+
f"Element {elem.get('id')} references non-existent nodes."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if "loads" in data:
|
|
35
|
+
loads = data["loads"]
|
|
36
|
+
if not isinstance(loads, list):
|
|
37
|
+
raise InputValidationError("'loads' must be a list of force objects.")
|
|
38
|
+
for lf in loads:
|
|
39
|
+
if not isinstance(lf, dict):
|
|
40
|
+
raise InputValidationError("Each load must be a dictionary.")
|
|
41
|
+
if "node_id" not in lf and "id" not in lf:
|
|
42
|
+
raise InputValidationError("Load missing 'node_id' or 'id'.")
|
|
43
|
+
if "Fx" not in lf and "Fy" not in lf:
|
|
44
|
+
raise InputValidationError("Load missing 'Fx' or 'Fy'.")
|
|
45
|
+
return data
|
truss_analysis/i18n.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""ماژول مدیریت چندزبانه (i18n) با پشتیبانی از Graceful Degradation"""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
TRANSLATIONS = {
|
|
7
|
+
"fa": {
|
|
8
|
+
"force": "نیرو",
|
|
9
|
+
"displacement": "جابجایی",
|
|
10
|
+
"node": "گره",
|
|
11
|
+
"element": "عضو",
|
|
12
|
+
"status": "وضعیت",
|
|
13
|
+
},
|
|
14
|
+
"en": {
|
|
15
|
+
"force": "Force",
|
|
16
|
+
"displacement": "Displacement",
|
|
17
|
+
"node": "Node",
|
|
18
|
+
"element": "Element",
|
|
19
|
+
"status": "Status",
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
CURRENT_LANG = "fa"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_text(key: str, lang: str = CURRENT_LANG) -> str:
|
|
26
|
+
lang_dict = TRANSLATIONS.get(lang, TRANSLATIONS.get("fa", {}))
|
|
27
|
+
if key in lang_dict:
|
|
28
|
+
return lang_dict[key]
|
|
29
|
+
logger.warning(f"Translation key not found: '{key}' in language '{lang}'")
|
|
30
|
+
return f"!!{key}!!"
|
truss_analysis/logger.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def setup_logger():
|
|
6
|
+
logger = logging.getLogger("truss_analysis")
|
|
7
|
+
if not logger.handlers:
|
|
8
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
9
|
+
handler.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
|
|
10
|
+
logger.addHandler(handler)
|
|
11
|
+
logger.setLevel(logging.INFO)
|
|
12
|
+
return logger
|
truss_analysis/main.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Main entry point for truss analysis with advanced CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from .assembly import assemble_global_matrices
|
|
10
|
+
from .fileio import load_json
|
|
11
|
+
from .model import Element, Node, validate_inputs
|
|
12
|
+
from .plotter import plot_axial_force, plot_truss, setup_persian_font
|
|
13
|
+
from .postprocess import (
|
|
14
|
+
calculate_element_forces,
|
|
15
|
+
calculate_reactions,
|
|
16
|
+
check_equilibrium,
|
|
17
|
+
)
|
|
18
|
+
from .report import to_csv, to_json, to_markdown
|
|
19
|
+
from .solver import check_energy, solve
|
|
20
|
+
from .units import to_si
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class AnalysisResult:
|
|
25
|
+
"""Complete analysis results."""
|
|
26
|
+
|
|
27
|
+
displacements: dict[str, tuple[float, float]]
|
|
28
|
+
forces: list[dict]
|
|
29
|
+
reactions: dict[str, dict[str, float]]
|
|
30
|
+
strain_energy: float
|
|
31
|
+
prestress_work: float
|
|
32
|
+
equilibrium_errors: dict[str, float]
|
|
33
|
+
nodes: list[Node] = field(default_factory=list)
|
|
34
|
+
elements: list[Element] = field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run(filepath: str, unit_sys: str = "SI") -> AnalysisResult:
|
|
38
|
+
"""Run complete truss analysis."""
|
|
39
|
+
data = load_json(filepath)
|
|
40
|
+
nodes = [
|
|
41
|
+
Node(
|
|
42
|
+
id=str(n["id"]),
|
|
43
|
+
x=to_si(n["x"], unit_sys, "L"),
|
|
44
|
+
y=to_si(n["y"], unit_sys, "L"),
|
|
45
|
+
is_support=n.get("is_support", False),
|
|
46
|
+
support_dx=n.get("support_dx", False),
|
|
47
|
+
support_dy=n.get("support_dy", False),
|
|
48
|
+
)
|
|
49
|
+
for n in data["nodes"]
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
elements = [
|
|
53
|
+
Element(
|
|
54
|
+
id=str(e["id"]),
|
|
55
|
+
node_i=str(e["node_i"]),
|
|
56
|
+
node_j=str(e["node_j"]),
|
|
57
|
+
E=to_si(e["E"], unit_sys, "E"),
|
|
58
|
+
A=to_si(e["A"], unit_sys, "A"),
|
|
59
|
+
I_sec=to_si(e.get("I_sec", e.get("I", 0.0)), unit_sys, "I_sec"),
|
|
60
|
+
alpha=to_si(e.get("alpha", 0.0), unit_sys, "alpha"),
|
|
61
|
+
delta_T=to_si(e.get("delta_T", 0.0), unit_sys, "delta_T"),
|
|
62
|
+
delta_L_free=to_si(e.get("delta_L_free", 0.0), unit_sys, "L"),
|
|
63
|
+
density=to_si(e.get("density", 0.0), unit_sys, "density"),
|
|
64
|
+
effective_length_factor=e.get("effective_length_factor", 1.0),
|
|
65
|
+
)
|
|
66
|
+
for e in data["elements"]
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
validate_inputs(nodes, elements)
|
|
70
|
+
K, F_ext, F_mechanical, fixed_dofs = assemble_global_matrices(nodes, elements)
|
|
71
|
+
|
|
72
|
+
# Add self-weight if density is provided
|
|
73
|
+
g = 9.81 # m/s²
|
|
74
|
+
for elem in elements:
|
|
75
|
+
if elem.density > 0:
|
|
76
|
+
i = next(j for j, n in enumerate(nodes) if n.id == elem.node_i)
|
|
77
|
+
j = next(j for j, n in enumerate(nodes) if n.id == elem.node_j)
|
|
78
|
+
dx = nodes[j].x - nodes[i].x
|
|
79
|
+
dy = nodes[j].y - nodes[i].y
|
|
80
|
+
L = (dx**2 + dy**2) ** 0.5
|
|
81
|
+
weight = elem.density * elem.A * L * g
|
|
82
|
+
F_ext[2 * i + 1] -= weight / 2
|
|
83
|
+
F_ext[2 * j + 1] -= weight / 2
|
|
84
|
+
F_mechanical[2 * i + 1] -= weight / 2
|
|
85
|
+
F_mechanical[2 * j + 1] -= weight / 2
|
|
86
|
+
|
|
87
|
+
loads = data.get("loads", [])
|
|
88
|
+
if isinstance(loads, dict):
|
|
89
|
+
loads = loads.get("node_forces", [])
|
|
90
|
+
|
|
91
|
+
node_map = {node.id: i for i, node in enumerate(nodes)}
|
|
92
|
+
for lf in loads:
|
|
93
|
+
nid = str(lf.get("node_id", lf.get("id")))
|
|
94
|
+
if nid in node_map:
|
|
95
|
+
idx = node_map[nid]
|
|
96
|
+
Fx = to_si(lf.get("Fx", 0.0), unit_sys, "F")
|
|
97
|
+
Fy = to_si(lf.get("Fy", 0.0), unit_sys, "F")
|
|
98
|
+
F_ext[idx * 2] += Fx
|
|
99
|
+
F_ext[idx * 2 + 1] += Fy
|
|
100
|
+
F_mechanical[idx * 2] += Fx
|
|
101
|
+
F_mechanical[idx * 2 + 1] += Fy
|
|
102
|
+
|
|
103
|
+
U = solve(K, F_ext, fixed_dofs)
|
|
104
|
+
results, strain_energy, prestress_work = calculate_element_forces(
|
|
105
|
+
nodes, elements, U
|
|
106
|
+
)
|
|
107
|
+
check_energy(U, F_mechanical, strain_energy, prestress_work)
|
|
108
|
+
|
|
109
|
+
reactions = calculate_reactions(nodes, elements, U, F_ext)
|
|
110
|
+
equilibrium_errors = check_equilibrium(nodes, reactions, F_ext)
|
|
111
|
+
displacements = {node.id: (U[i * 2], U[i * 2 + 1]) for i, node in enumerate(nodes)}
|
|
112
|
+
|
|
113
|
+
return AnalysisResult(
|
|
114
|
+
displacements=displacements,
|
|
115
|
+
forces=results,
|
|
116
|
+
reactions=reactions,
|
|
117
|
+
strain_energy=strain_energy,
|
|
118
|
+
prestress_work=prestress_work,
|
|
119
|
+
equilibrium_errors=equilibrium_errors,
|
|
120
|
+
nodes=nodes,
|
|
121
|
+
elements=elements,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main() -> int:
|
|
126
|
+
# Set UTF-8 encoding for stdout/stderr (Windows compatibility)
|
|
127
|
+
import io
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")
|
|
131
|
+
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8")
|
|
132
|
+
except (AttributeError, Exception):
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
parser = argparse.ArgumentParser(
|
|
136
|
+
description="2D Truss Analysis Tool with Advanced Features",
|
|
137
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
138
|
+
epilog="""
|
|
139
|
+
Examples:
|
|
140
|
+
python main.py example.json
|
|
141
|
+
python main.py example.json --plot
|
|
142
|
+
python main.py example.json --output results --format all
|
|
143
|
+
python main.py example.json --save-plots --scale 50
|
|
144
|
+
python main.py example.json --check-buckling
|
|
145
|
+
""",
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument("filepath", help="Path to the JSON input file")
|
|
148
|
+
parser.add_argument(
|
|
149
|
+
"unit_sys",
|
|
150
|
+
nargs="?",
|
|
151
|
+
default="SI",
|
|
152
|
+
help="Unit system (SI or Imperial)",
|
|
153
|
+
)
|
|
154
|
+
parser.add_argument(
|
|
155
|
+
"--plot",
|
|
156
|
+
action="store_true",
|
|
157
|
+
help="Show interactive plots of the truss",
|
|
158
|
+
)
|
|
159
|
+
parser.add_argument(
|
|
160
|
+
"--save-plots",
|
|
161
|
+
action="store_true",
|
|
162
|
+
help="Save plots as PNG files",
|
|
163
|
+
)
|
|
164
|
+
parser.add_argument(
|
|
165
|
+
"--output",
|
|
166
|
+
type=str,
|
|
167
|
+
help="Output file prefix (default: results)",
|
|
168
|
+
)
|
|
169
|
+
parser.add_argument(
|
|
170
|
+
"--format",
|
|
171
|
+
choices=["json", "csv", "markdown", "all"],
|
|
172
|
+
default="all",
|
|
173
|
+
help="Output format(s) to generate",
|
|
174
|
+
)
|
|
175
|
+
parser.add_argument(
|
|
176
|
+
"--check-buckling",
|
|
177
|
+
action="store_true",
|
|
178
|
+
help="Check Euler buckling for compression members",
|
|
179
|
+
)
|
|
180
|
+
parser.add_argument(
|
|
181
|
+
"--scale",
|
|
182
|
+
type=float,
|
|
183
|
+
default=10.0,
|
|
184
|
+
help="Deformation scale factor for plots (default: 10)",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
args = parser.parse_args()
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
result = run(args.filepath, args.unit_sys)
|
|
191
|
+
|
|
192
|
+
# Console output
|
|
193
|
+
print("✅ تحلیل با موفقیت انجام شد.")
|
|
194
|
+
print(f"انرژی کرنشی: {result.strain_energy:.4f} J")
|
|
195
|
+
print(f"کار پیشتنیدگی: {result.prestress_work:.4f} J")
|
|
196
|
+
print("\n📊 نیروهای اعضا:")
|
|
197
|
+
for r in result.forces:
|
|
198
|
+
status = "📈 کشش" if r["force"] > 0 else "📉 فشار"
|
|
199
|
+
print(f" المان {r['element']}: {r['force']:.2f} N ({status})")
|
|
200
|
+
if r.get("buckling_warning"):
|
|
201
|
+
print(f" ⚠️ {r['buckling_warning']}")
|
|
202
|
+
|
|
203
|
+
print("\n🔧 عکسالعملهای تکیهگاهی:")
|
|
204
|
+
for node_id, react in result.reactions.items():
|
|
205
|
+
print(f" گره {node_id}: Rx={react['Rx']:.2f} N, Ry={react['Ry']:.2f} N")
|
|
206
|
+
|
|
207
|
+
print("\n✅ بررسی تعادل استاتیکی:")
|
|
208
|
+
print(f" خطای ΣFx: {result.equilibrium_errors['delta_Fx']:.6e}")
|
|
209
|
+
print(f" خطای ΣFy: {result.equilibrium_errors['delta_Fy']:.6e}")
|
|
210
|
+
print(f" خطای ΣM: {result.equilibrium_errors['delta_M']:.6e}")
|
|
211
|
+
|
|
212
|
+
# Generate plots if requested
|
|
213
|
+
if args.plot or args.save_plots:
|
|
214
|
+
font_prop = setup_persian_font()
|
|
215
|
+
prefix = args.output or "results"
|
|
216
|
+
|
|
217
|
+
if args.save_plots:
|
|
218
|
+
plot_truss(
|
|
219
|
+
result.nodes,
|
|
220
|
+
result.elements,
|
|
221
|
+
result.displacements,
|
|
222
|
+
show=args.plot,
|
|
223
|
+
filename=f"{prefix}_truss.png",
|
|
224
|
+
scale=args.scale,
|
|
225
|
+
font_prop=font_prop,
|
|
226
|
+
)
|
|
227
|
+
plot_axial_force(
|
|
228
|
+
result.nodes,
|
|
229
|
+
result.elements,
|
|
230
|
+
result.forces,
|
|
231
|
+
filename=f"{prefix}_forces.png",
|
|
232
|
+
font_prop=font_prop,
|
|
233
|
+
)
|
|
234
|
+
elif args.plot:
|
|
235
|
+
plot_truss(
|
|
236
|
+
result.nodes,
|
|
237
|
+
result.elements,
|
|
238
|
+
result.displacements,
|
|
239
|
+
show=True,
|
|
240
|
+
scale=args.scale,
|
|
241
|
+
font_prop=font_prop,
|
|
242
|
+
)
|
|
243
|
+
plot_axial_force(
|
|
244
|
+
result.nodes,
|
|
245
|
+
result.elements,
|
|
246
|
+
result.forces,
|
|
247
|
+
font_prop=font_prop,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
# Generate structured outputs
|
|
251
|
+
if args.output:
|
|
252
|
+
prefix = args.output
|
|
253
|
+
if args.format in ["json", "all"]:
|
|
254
|
+
to_json(result, f"{prefix}.json")
|
|
255
|
+
if args.format in ["csv", "all"]:
|
|
256
|
+
to_csv(result, prefix)
|
|
257
|
+
if args.format in ["markdown", "all"]:
|
|
258
|
+
to_markdown(result, f"{prefix}.md")
|
|
259
|
+
|
|
260
|
+
return 0
|
|
261
|
+
|
|
262
|
+
except Exception as e:
|
|
263
|
+
print(f"❌ خطا در تحلیل: {e}", file=sys.stderr)
|
|
264
|
+
import traceback
|
|
265
|
+
|
|
266
|
+
traceback.print_exc()
|
|
267
|
+
return 2
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
if __name__ == "__main__":
|
|
271
|
+
sys.exit(main())
|
truss_analysis/model.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Pure DTOs for Truss Analysis."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .exceptions import InputValidationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Node:
|
|
12
|
+
"""A node in the truss structure."""
|
|
13
|
+
|
|
14
|
+
id: str
|
|
15
|
+
x: float
|
|
16
|
+
y: float
|
|
17
|
+
is_support: bool = False
|
|
18
|
+
support_dx: bool = False
|
|
19
|
+
support_dy: bool = False
|
|
20
|
+
|
|
21
|
+
def __post_init__(self):
|
|
22
|
+
if not isinstance(self.id, str):
|
|
23
|
+
raise InputValidationError(f"Node ID must be string, got {type(self.id)}")
|
|
24
|
+
if not all(isinstance(v, (int, float)) for v in [self.x, self.y]):
|
|
25
|
+
raise InputValidationError("Node coordinates must be numeric")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Element:
|
|
30
|
+
"""A truss element (bar) connecting two nodes."""
|
|
31
|
+
|
|
32
|
+
id: str
|
|
33
|
+
node_i: str
|
|
34
|
+
node_j: str
|
|
35
|
+
E: float # Young's modulus
|
|
36
|
+
A: float # Cross-sectional area
|
|
37
|
+
I_sec: float = 0.0 # Second moment of area
|
|
38
|
+
alpha: float = 0.0 # Thermal expansion coefficient
|
|
39
|
+
delta_T: float = 0.0 # Temperature change
|
|
40
|
+
delta_L_free: float = 0.0 # Free length change (fabrication error)
|
|
41
|
+
density: float = 0.0 # Material density (for self-weight)
|
|
42
|
+
effective_length_factor: float = 1.0 # For buckling calculation
|
|
43
|
+
|
|
44
|
+
def __post_init__(self):
|
|
45
|
+
if not isinstance(self.id, str):
|
|
46
|
+
raise InputValidationError(
|
|
47
|
+
f"Element ID must be string, got {type(self.id)}"
|
|
48
|
+
)
|
|
49
|
+
if self.E <= 0:
|
|
50
|
+
raise InputValidationError(
|
|
51
|
+
f"Element {self.id}: E must be positive, got {self.E}"
|
|
52
|
+
)
|
|
53
|
+
if self.A <= 0:
|
|
54
|
+
raise InputValidationError(
|
|
55
|
+
f"Element {self.id}: A must be positive, got {self.A}"
|
|
56
|
+
)
|
|
57
|
+
if self.node_i == self.node_j:
|
|
58
|
+
raise InputValidationError(
|
|
59
|
+
f"Element {self.id}: node_i and node_j cannot be the same"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def validate_inputs(nodes: list[Node], elements: list[Element]) -> None:
|
|
64
|
+
"""Validate input data for consistency and correctness."""
|
|
65
|
+
|
|
66
|
+
# Check unique node IDs
|
|
67
|
+
node_ids = {n.id for n in nodes}
|
|
68
|
+
if len(node_ids) != len(nodes):
|
|
69
|
+
raise InputValidationError("Duplicate node IDs found")
|
|
70
|
+
|
|
71
|
+
# Check unique element IDs
|
|
72
|
+
elem_ids = {e.id for e in elements}
|
|
73
|
+
if len(elem_ids) != len(elements):
|
|
74
|
+
raise InputValidationError("Duplicate element IDs found")
|
|
75
|
+
|
|
76
|
+
# Check that all element node references exist
|
|
77
|
+
for elem in elements:
|
|
78
|
+
if elem.node_i not in node_ids:
|
|
79
|
+
raise InputValidationError(
|
|
80
|
+
f"Element {elem.id} references non-existent node {elem.node_i}"
|
|
81
|
+
)
|
|
82
|
+
if elem.node_j not in node_ids:
|
|
83
|
+
raise InputValidationError(
|
|
84
|
+
f"Element {elem.id} references non-existent node {elem.node_j}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Check kinematic stability (at least 3 constraints total)
|
|
88
|
+
total_constraints = sum(
|
|
89
|
+
(n.support_dx + n.support_dy) for n in nodes if n.is_support
|
|
90
|
+
)
|
|
91
|
+
if total_constraints < 3:
|
|
92
|
+
raise InputValidationError(
|
|
93
|
+
f"Insufficient constraints for stability: {total_constraints} < 3"
|
|
94
|
+
)
|