ANYsolver 0.1.0__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.
- anysolver/__init__.py +631 -0
- anysolver/anystructure_fem_mode.py +942 -0
- anysolver/arc_length.py +758 -0
- anysolver/assembly.py +950 -0
- anysolver/baselines.py +303 -0
- anysolver/beam_shell_verification.py +4276 -0
- anysolver/beam_validity.py +110 -0
- anysolver/benchmarks.py +495 -0
- anysolver/boundary.py +476 -0
- anysolver/buckling.py +442 -0
- anysolver/buckling_validity.py +91 -0
- anysolver/capacity_workflow.py +350 -0
- anysolver/cases.py +173 -0
- anysolver/composite_strip_verification.py +297 -0
- anysolver/contact.py +3461 -0
- anysolver/corotational.py +371 -0
- anysolver/cylinder_benchmarks.py +364 -0
- anysolver/dynamics.py +723 -0
- anysolver/element_qualification.py +432 -0
- anysolver/elements.py +2724 -0
- anysolver/external_references.py +369 -0
- anysolver/fe_core.py +327 -0
- anysolver/fracture.py +551 -0
- anysolver/imperfections.py +390 -0
- anysolver/jit_compiler.py +108 -0
- anysolver/kernel_warmup.py +180 -0
- anysolver/linalg.py +760 -0
- anysolver/mass_properties.py +179 -0
- anysolver/material_curves.py +231 -0
- anysolver/matrix_assembly.py +558 -0
- anysolver/mesh_gen.py +1065 -0
- anysolver/mesh_load_bc_verification.py +609 -0
- anysolver/modal.py +282 -0
- anysolver/nonlinear.py +300 -0
- anysolver/nonlinear_performance.py +920 -0
- anysolver/nonlinear_performance_batch_b.py +592 -0
- anysolver/nonlinear_performance_batch_c.py +506 -0
- anysolver/nonlinear_performance_bootstrap.py +120 -0
- anysolver/nonlinear_reduced_assembly.py +760 -0
- anysolver/nonlinear_static.py +1518 -0
- anysolver/plasticity.py +419 -0
- anysolver/plasticity_qualification.py +314 -0
- anysolver/production_readiness.py +304 -0
- anysolver/quality_control.py +1497 -0
- anysolver/recovery.py +505 -0
- anysolver/recovery_policy.py +205 -0
- anysolver/reference_cases.py +425 -0
- anysolver/results.py +503 -0
- anysolver/runtime.py +9030 -0
- anysolver/s4_validity.py +326 -0
- anysolver/sesam_fem/__init__.py +55 -0
- anysolver/sesam_fem/__main__.py +80 -0
- anysolver/sesam_fem/diagnostics.py +65 -0
- anysolver/sesam_fem/document.py +814 -0
- anysolver/sesam_fem/exporter.py +84 -0
- anysolver/sesam_fem/importer.py +365 -0
- anysolver/sesam_fem/records.py +257 -0
- anysolver/sesam_fem/schema.py +107 -0
- anysolver/sesam_fem/sif_importer.py +397 -0
- anysolver/sesam_fem/validation.py +62 -0
- anysolver/shell_benchmarks.py +367 -0
- anysolver/test_cases.py +610 -0
- anysolver/validation.py +416 -0
- anysolver/vectorized_nonlinear.py +334 -0
- anysolver/vectorized_stiffness.py +571 -0
- anysolver-0.1.0.dist-info/METADATA +165 -0
- anysolver-0.1.0.dist-info/RECORD +70 -0
- anysolver-0.1.0.dist-info/WHEEL +5 -0
- anysolver-0.1.0.dist-info/licenses/LICENSE +674 -0
- anysolver-0.1.0.dist-info/top_level.txt +1 -0
anysolver/test_cases.py
ADDED
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive Test Cases for FE Solver
|
|
3
|
+
|
|
4
|
+
This module provides realistic test cases for ship structural analysis,
|
|
5
|
+
including comparisons with semi-analytical solutions where available.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import TYPE_CHECKING, List, Dict, Tuple, Optional, Any
|
|
11
|
+
import numpy as np
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .fe_core import FEModel
|
|
18
|
+
from .boundary import LoadCase
|
|
19
|
+
from .results import FEResult
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class TestCase:
|
|
24
|
+
"""Base class for test cases."""
|
|
25
|
+
name: str
|
|
26
|
+
description: str
|
|
27
|
+
category: str = "General"
|
|
28
|
+
|
|
29
|
+
def run(self) -> Dict[str, Any]:
|
|
30
|
+
"""Run the test case."""
|
|
31
|
+
raise NotImplementedError
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
34
|
+
return {
|
|
35
|
+
'name': self.name,
|
|
36
|
+
'description': self.description,
|
|
37
|
+
'category': self.category
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class ShipPanelTestCase(TestCase):
|
|
43
|
+
"""
|
|
44
|
+
Test Case: Typical Ship Stiffened Panel
|
|
45
|
+
|
|
46
|
+
This test case models a typical ship panel with:
|
|
47
|
+
- Plate: 2000 x 1000 mm, 15 mm thick
|
|
48
|
+
- Stiffeners: T-bar, 100 mm web, 8 mm web thickness, 100x12 mm flange
|
|
49
|
+
- Spacing: 600 mm
|
|
50
|
+
- Material: Ship steel (E=210 GPa, ν=0.3, σy=235 MPa)
|
|
51
|
+
- Loading: Lateral pressure + in-plane loads
|
|
52
|
+
|
|
53
|
+
Includes simplified checks inspired by DNV-CG-0128 guidance.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# Geometry parameters (mm)
|
|
57
|
+
panel_length: float = 2000.0
|
|
58
|
+
panel_width: float = 1000.0
|
|
59
|
+
plate_thickness: float = 15.0
|
|
60
|
+
|
|
61
|
+
# Stiffener parameters (mm)
|
|
62
|
+
stiffener_type: str = "T-bar"
|
|
63
|
+
stiffener_spacing: float = 600.0
|
|
64
|
+
web_height: float = 100.0
|
|
65
|
+
web_thickness: float = 8.0
|
|
66
|
+
flange_width: float = 100.0
|
|
67
|
+
flange_thickness: float = 12.0
|
|
68
|
+
|
|
69
|
+
# Material properties
|
|
70
|
+
elastic_modulus: float = 210e9 # Pa
|
|
71
|
+
poisson_ratio: float = 0.3
|
|
72
|
+
yield_stress: float = 235e6 # Pa
|
|
73
|
+
|
|
74
|
+
# Loading
|
|
75
|
+
lateral_pressure: float = 0.05e6 # Pa (50 kPa)
|
|
76
|
+
axial_stress: float = 100e6 # Pa (100 MPa)
|
|
77
|
+
transverse_stress: float = 50e6 # Pa
|
|
78
|
+
shear_stress: float = 30e6 # Pa
|
|
79
|
+
|
|
80
|
+
# Boundary conditions
|
|
81
|
+
in_plane_support: str = "Integrated"
|
|
82
|
+
rotational_support: str = "SS"
|
|
83
|
+
|
|
84
|
+
# Mesh settings
|
|
85
|
+
shell_divisions_x: int = 8
|
|
86
|
+
shell_divisions_y: int = 4
|
|
87
|
+
beam_divisions: int = 4
|
|
88
|
+
|
|
89
|
+
def __init__(self):
|
|
90
|
+
super().__init__(
|
|
91
|
+
name="Ship Stiffened Panel",
|
|
92
|
+
description="Typical ship panel with T-bar stiffeners under combined loading",
|
|
93
|
+
category="Ship Structures"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def run(self) -> Dict[str, Any]:
|
|
97
|
+
"""Run the ship panel test case."""
|
|
98
|
+
from . import (
|
|
99
|
+
PanelGeometry, MeshConfig, generate_stiffened_panel_mesh,
|
|
100
|
+
LoadCase, solve_linear, create_fe_result, FixedSupport
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
result = {
|
|
104
|
+
'test_case': self.name,
|
|
105
|
+
'geometry': self.to_dict(),
|
|
106
|
+
'results': {},
|
|
107
|
+
'comparisons': {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Create panel geometry
|
|
111
|
+
panel = PanelGeometry(
|
|
112
|
+
length=self.panel_length / 1000, # Convert to meters
|
|
113
|
+
width=self.panel_width / 1000,
|
|
114
|
+
plate_thickness=self.plate_thickness / 1000,
|
|
115
|
+
stiffener_type=self.stiffener_type,
|
|
116
|
+
stiffener_spacing=self.stiffener_spacing / 1000,
|
|
117
|
+
stiffener_height=self.web_height / 1000,
|
|
118
|
+
stiffener_web_thickness=self.web_thickness / 1000,
|
|
119
|
+
stiffener_flange_width=self.flange_width / 1000,
|
|
120
|
+
stiffener_flange_thickness=self.flange_thickness / 1000,
|
|
121
|
+
num_stiffeners=int(self.panel_width / self.stiffener_spacing),
|
|
122
|
+
in_plane_support=self.in_plane_support,
|
|
123
|
+
rotational_support=self.rotational_support,
|
|
124
|
+
axial_stress=self.axial_stress,
|
|
125
|
+
transverse_stress=self.transverse_stress,
|
|
126
|
+
shear_stress=self.shear_stress,
|
|
127
|
+
pressure=self.lateral_pressure
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Create mesh configuration
|
|
131
|
+
config = MeshConfig(
|
|
132
|
+
shell_num_divisions_x=self.shell_divisions_x,
|
|
133
|
+
shell_num_divisions_y=self.shell_divisions_y,
|
|
134
|
+
beam_num_divisions=self.beam_divisions,
|
|
135
|
+
use_coupling_elements=True,
|
|
136
|
+
coupling_stiffness=1e12
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Generate mesh
|
|
140
|
+
model = generate_stiffened_panel_mesh(panel, config)
|
|
141
|
+
|
|
142
|
+
result['results']['mesh'] = {
|
|
143
|
+
'num_nodes': model.mesh.num_nodes,
|
|
144
|
+
'num_elements': model.mesh.num_elements,
|
|
145
|
+
'num_shell_elements': len([e for e in model.mesh.elements.values() if e.__class__.__name__ == 'ShellElement']),
|
|
146
|
+
'num_beam_elements': len([e for e in model.mesh.elements.values() if e.__class__.__name__ == 'BeamElement']),
|
|
147
|
+
'num_coupling_elements': len([e for e in model.mesh.elements.values() if e.__class__.__name__ == 'CoupledBeamShellElement'])
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Add edge fixity only to shell/master nodes. Beam nodes may be MPC
|
|
151
|
+
# slaves in the eccentric beam-shell coupling and must not be fixed.
|
|
152
|
+
edge_nodes = []
|
|
153
|
+
L = panel.length
|
|
154
|
+
W = panel.width
|
|
155
|
+
for node_id, node in model.mesh.nodes.items():
|
|
156
|
+
x, y, z = node.x, node.y, node.z
|
|
157
|
+
if (abs(z) < 1e-9 and
|
|
158
|
+
(abs(x) < 1e-6 or abs(x - L) < 1e-6 or
|
|
159
|
+
abs(y) < 1e-6 or abs(y - W) < 1e-6)):
|
|
160
|
+
edge_nodes.append(node_id)
|
|
161
|
+
|
|
162
|
+
for node_id in edge_nodes:
|
|
163
|
+
model.add_boundary_condition(FixedSupport(f"Edge_{node_id}", [node_id]))
|
|
164
|
+
|
|
165
|
+
# Create load case
|
|
166
|
+
load_case = LoadCase(name="combined_loading")
|
|
167
|
+
|
|
168
|
+
# Apply lateral pressure to all shell elements
|
|
169
|
+
for elem_id, element in model.mesh.elements.items():
|
|
170
|
+
if element.__class__.__name__ == 'ShellElement':
|
|
171
|
+
load_case.add_pressure_load(elem_id, pressure=self.lateral_pressure)
|
|
172
|
+
|
|
173
|
+
# Apply in-plane loads (simplified as nodal loads)
|
|
174
|
+
# This would be more accurate with proper in-plane load application
|
|
175
|
+
|
|
176
|
+
# Solve
|
|
177
|
+
start_time = time.time()
|
|
178
|
+
displacements, solver_info = solve_linear(model, load_case, solver_type='direct')
|
|
179
|
+
solve_time = time.time() - start_time
|
|
180
|
+
|
|
181
|
+
result['results']['solver'] = {
|
|
182
|
+
'solve_time': solve_time,
|
|
183
|
+
'convergence': solver_info.get('convergence_info', {}).get('status', 'unknown'),
|
|
184
|
+
'num_free_dofs': solver_info.get('num_free_dofs', 0),
|
|
185
|
+
'num_constrained_dofs': solver_info.get('num_constrained_dofs', 0)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# Create result object
|
|
189
|
+
solver_info_copy = solver_info.copy()
|
|
190
|
+
solver_info_copy['load_case'] = load_case
|
|
191
|
+
fe_result = create_fe_result(model, displacements, solver_info_copy)
|
|
192
|
+
|
|
193
|
+
# Extract key results
|
|
194
|
+
max_disp = fe_result.max_displacement
|
|
195
|
+
max_disp_node = fe_result.max_displacement_node
|
|
196
|
+
|
|
197
|
+
result['results']['displacements'] = {
|
|
198
|
+
'max_displacement': max_disp,
|
|
199
|
+
'max_displacement_node': max_disp_node,
|
|
200
|
+
'displacement_norm': fe_result.get_displacement_norm()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# Calculate stresses (simplified)
|
|
204
|
+
# For a more complete analysis, we would compute stresses at integration points
|
|
205
|
+
|
|
206
|
+
result['results']['stresses'] = {
|
|
207
|
+
'note': 'Stress calculation would be added in post-processing'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
# Design checks
|
|
211
|
+
result['design_checks'] = self.perform_design_checks(fe_result, panel)
|
|
212
|
+
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
def perform_design_checks(self, fe_result: 'FEResult', panel: 'PanelGeometry') -> Dict[str, Any]:
|
|
216
|
+
"""Perform basic design checks."""
|
|
217
|
+
checks = {}
|
|
218
|
+
|
|
219
|
+
# Check 1: Displacement limit (typical: L/200 for serviceability)
|
|
220
|
+
L = panel.length
|
|
221
|
+
max_disp = fe_result.max_displacement
|
|
222
|
+
displacement_limit = L / 200
|
|
223
|
+
displacement_utilization = max_disp / displacement_limit if displacement_limit > 0 else 0
|
|
224
|
+
|
|
225
|
+
checks['displacement'] = {
|
|
226
|
+
'max_displacement': max_disp,
|
|
227
|
+
'limit': displacement_limit,
|
|
228
|
+
'utilization': displacement_utilization,
|
|
229
|
+
'status': 'OK' if displacement_utilization < 1.0 else 'EXCEEDS'
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Check 2: Stress limit (yield stress)
|
|
233
|
+
# Note: Actual stress calculation would be needed
|
|
234
|
+
checks['stress'] = {
|
|
235
|
+
'yield_stress': self.yield_stress,
|
|
236
|
+
'note': 'Stress check requires stress recovery from elements',
|
|
237
|
+
'status': 'NOT_AVAILABLE'
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Check 3: Buckling check (simplified)
|
|
241
|
+
# Based on plate slenderness
|
|
242
|
+
plate_slenderness = (panel.width / panel.plate_thickness) * np.sqrt(
|
|
243
|
+
self.yield_stress / (210e9 * np.pi**2)
|
|
244
|
+
)
|
|
245
|
+
checks['buckling'] = {
|
|
246
|
+
'plate_slenderness': plate_slenderness,
|
|
247
|
+
'limit': 200.0, # Typical limit from DNV
|
|
248
|
+
'status': 'OK' if plate_slenderness < 200.0 else 'EXCEEDS'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return checks
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@dataclass
|
|
255
|
+
class BucklingTestCase(TestCase):
|
|
256
|
+
"""
|
|
257
|
+
Test Case: Plate Buckling Under Compression
|
|
258
|
+
|
|
259
|
+
This test case verifies the solver's ability to capture buckling behavior
|
|
260
|
+
by comparing with analytical buckling coefficients.
|
|
261
|
+
|
|
262
|
+
Analytical solution for simply supported plate:
|
|
263
|
+
σ_cr = k * π² * E / (12 * (1-ν²)) * (t/b)²
|
|
264
|
+
|
|
265
|
+
Where k is the buckling coefficient (4.0 for simply supported square plate)
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
panel_length: float = 1000.0 # mm
|
|
269
|
+
panel_width: float = 1000.0 # mm
|
|
270
|
+
plate_thickness: float = 10.0 # mm
|
|
271
|
+
|
|
272
|
+
elastic_modulus: float = 210e9 # Pa
|
|
273
|
+
poisson_ratio: float = 0.3
|
|
274
|
+
|
|
275
|
+
# Buckling coefficient for simply supported square plate
|
|
276
|
+
buckling_coefficient: float = 4.0
|
|
277
|
+
|
|
278
|
+
def __init__(self):
|
|
279
|
+
super().__init__(
|
|
280
|
+
name="Plate Buckling",
|
|
281
|
+
description="Plate buckling under uniform compression",
|
|
282
|
+
category="Stability"
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def run(self) -> Dict[str, Any]:
|
|
286
|
+
"""Run buckling test case."""
|
|
287
|
+
from . import generate_simple_panel_mesh, LoadCase, solve_linear
|
|
288
|
+
|
|
289
|
+
result = {
|
|
290
|
+
'test_case': self.name,
|
|
291
|
+
'parameters': self.to_dict(),
|
|
292
|
+
'analytical': {},
|
|
293
|
+
'fe_results': {},
|
|
294
|
+
'comparisons': {}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# Calculate analytical buckling stress
|
|
298
|
+
b = self.panel_width / 1000 # Convert to meters
|
|
299
|
+
t = self.plate_thickness / 1000
|
|
300
|
+
|
|
301
|
+
sigma_cr_analytical = (self.buckling_coefficient * np.pi**2 * self.elastic_modulus /
|
|
302
|
+
(12 * (1 - self.poisson_ratio**2))) * (t / b)**2
|
|
303
|
+
|
|
304
|
+
result['analytical'] = {
|
|
305
|
+
'buckling_stress': sigma_cr_analytical,
|
|
306
|
+
'buckling_load': sigma_cr_analytical * t * b * 1000 # For 1m length
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# Create FE model
|
|
310
|
+
model = generate_simple_panel_mesh(
|
|
311
|
+
length=self.panel_length / 1000,
|
|
312
|
+
width=self.panel_width / 1000,
|
|
313
|
+
thickness=self.plate_thickness / 1000,
|
|
314
|
+
num_divisions_x=10,
|
|
315
|
+
num_divisions_y=10
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# Apply compressive load (as in-plane load)
|
|
319
|
+
# For linear analysis, we can only apply a fraction of the buckling load
|
|
320
|
+
applied_stress = sigma_cr_analytical * 0.5 # 50% of buckling stress
|
|
321
|
+
|
|
322
|
+
load_case = LoadCase(name="compression")
|
|
323
|
+
# Apply compressive stress to all nodes on one edge
|
|
324
|
+
for node_id, node in model.mesh.nodes.items():
|
|
325
|
+
if abs(node.y) < 1e-6: # Bottom edge
|
|
326
|
+
# Apply compressive force
|
|
327
|
+
area = (self.plate_thickness / 1000) * (self.panel_length / 1000 / 10)
|
|
328
|
+
force = applied_stress * area
|
|
329
|
+
load_case.add_nodal_load(node_id, forces=np.array([-force, 0, 0]))
|
|
330
|
+
|
|
331
|
+
# Fix the other edge
|
|
332
|
+
from .boundary import FixedSupport
|
|
333
|
+
for node_id, node in model.mesh.nodes.items():
|
|
334
|
+
if abs(node.y - self.panel_width / 1000) < 1e-6: # Top edge
|
|
335
|
+
model.add_boundary_condition(FixedSupport(f"Fixed_{node_id}", [node_id]))
|
|
336
|
+
|
|
337
|
+
# Solve
|
|
338
|
+
try:
|
|
339
|
+
displacements, solver_info = solve_linear(model, load_case, solver_type='direct')
|
|
340
|
+
max_disp = float(np.max(np.abs(displacements)))
|
|
341
|
+
|
|
342
|
+
result['fe_results'] = {
|
|
343
|
+
'max_displacement': max_disp,
|
|
344
|
+
'applied_stress': applied_stress,
|
|
345
|
+
'buckling_stress_analytical': sigma_cr_analytical,
|
|
346
|
+
'stress_ratio': applied_stress / sigma_cr_analytical
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# Note: Linear analysis cannot capture buckling directly
|
|
350
|
+
# This would require nonlinear analysis or eigenvalue analysis
|
|
351
|
+
result['comparisons'] = {
|
|
352
|
+
'note': 'Linear analysis cannot capture buckling. Eigenvalue analysis needed.',
|
|
353
|
+
'recommendation': 'Implement eigenvalue solver for buckling analysis'
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
except Exception as e:
|
|
357
|
+
result['fe_results']['error'] = str(e)
|
|
358
|
+
|
|
359
|
+
return result
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@dataclass
|
|
363
|
+
class VibrationTestCase(TestCase):
|
|
364
|
+
"""
|
|
365
|
+
Test Case: Natural Frequency Analysis
|
|
366
|
+
|
|
367
|
+
This test case demonstrates how to perform modal analysis
|
|
368
|
+
to find natural frequencies of a stiffened panel.
|
|
369
|
+
|
|
370
|
+
Note: This requires eigenvalue solver implementation.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
panel_length: float = 2000.0 # mm
|
|
374
|
+
panel_width: float = 1000.0 # mm
|
|
375
|
+
plate_thickness: float = 15.0 # mm
|
|
376
|
+
|
|
377
|
+
stiffener_spacing: float = 600.0 # mm
|
|
378
|
+
web_height: float = 100.0 # mm
|
|
379
|
+
web_thickness: float = 8.0 # mm
|
|
380
|
+
flange_width: float = 100.0 # mm
|
|
381
|
+
flange_thickness: float = 12.0 # mm
|
|
382
|
+
|
|
383
|
+
elastic_modulus: float = 210e9 # Pa
|
|
384
|
+
poisson_ratio: float = 0.3
|
|
385
|
+
density: float = 7850.0 # kg/m^3
|
|
386
|
+
|
|
387
|
+
def __init__(self):
|
|
388
|
+
super().__init__(
|
|
389
|
+
name="Natural Frequency",
|
|
390
|
+
description="Modal analysis of stiffened panel",
|
|
391
|
+
category="Dynamics"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
def run(self) -> Dict[str, Any]:
|
|
395
|
+
"""Run vibration test case."""
|
|
396
|
+
from . import PanelGeometry, MeshConfig, generate_stiffened_panel_mesh
|
|
397
|
+
|
|
398
|
+
result = {
|
|
399
|
+
'test_case': self.name,
|
|
400
|
+
'parameters': self.to_dict(),
|
|
401
|
+
'results': {},
|
|
402
|
+
'notes': []
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
# Create panel
|
|
406
|
+
panel = PanelGeometry(
|
|
407
|
+
length=self.panel_length / 1000,
|
|
408
|
+
width=self.panel_width / 1000,
|
|
409
|
+
plate_thickness=self.plate_thickness / 1000,
|
|
410
|
+
stiffener_type="T-bar",
|
|
411
|
+
stiffener_spacing=self.stiffener_spacing / 1000,
|
|
412
|
+
stiffener_height=self.web_height / 1000,
|
|
413
|
+
stiffener_web_thickness=self.web_thickness / 1000,
|
|
414
|
+
stiffener_flange_width=self.flange_width / 1000,
|
|
415
|
+
stiffener_flange_thickness=self.flange_thickness / 1000,
|
|
416
|
+
num_stiffeners=1
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
config = MeshConfig(
|
|
420
|
+
shell_num_divisions_x=8,
|
|
421
|
+
shell_num_divisions_y=4,
|
|
422
|
+
beam_num_divisions=8,
|
|
423
|
+
use_coupling_elements=True
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
model = generate_stiffened_panel_mesh(panel, config)
|
|
427
|
+
|
|
428
|
+
result['results'] = {
|
|
429
|
+
'mesh': {
|
|
430
|
+
'num_nodes': model.mesh.num_nodes,
|
|
431
|
+
'num_elements': model.mesh.num_elements
|
|
432
|
+
},
|
|
433
|
+
'material': {
|
|
434
|
+
'elastic_modulus': self.elastic_modulus,
|
|
435
|
+
'density': self.density,
|
|
436
|
+
'poisson_ratio': self.poisson_ratio
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
result['notes'].append("Eigenvalue solver for modal analysis not yet implemented")
|
|
441
|
+
result['notes'].append("Would solve: (K - ω²M)u = 0 for natural frequencies")
|
|
442
|
+
result['notes'].append("Expected first mode: ~10-20 Hz for typical ship panel")
|
|
443
|
+
|
|
444
|
+
return result
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@dataclass
|
|
448
|
+
class ThermalLoadTestCase(TestCase):
|
|
449
|
+
"""
|
|
450
|
+
Test Case: Thermal Loading
|
|
451
|
+
|
|
452
|
+
This test case demonstrates thermal load analysis.
|
|
453
|
+
|
|
454
|
+
Note: Thermal analysis requires additional implementation.
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
panel_length: float = 1000.0 # mm
|
|
458
|
+
panel_width: float = 1000.0 # mm
|
|
459
|
+
plate_thickness: float = 10.0 # mm
|
|
460
|
+
|
|
461
|
+
elastic_modulus: float = 210e9 # Pa
|
|
462
|
+
poisson_ratio: float = 0.3
|
|
463
|
+
thermal_expansion: float = 12e-6 # 1/°C for steel
|
|
464
|
+
|
|
465
|
+
temperature_change: float = 50.0 # °C
|
|
466
|
+
|
|
467
|
+
def __init__(self):
|
|
468
|
+
super().__init__(
|
|
469
|
+
name="Thermal Loading",
|
|
470
|
+
description="Thermal expansion analysis",
|
|
471
|
+
category="Thermal"
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
def run(self) -> Dict[str, Any]:
|
|
475
|
+
"""Run thermal load test case."""
|
|
476
|
+
result = {
|
|
477
|
+
'test_case': self.name,
|
|
478
|
+
'parameters': self.to_dict(),
|
|
479
|
+
'analytical': {},
|
|
480
|
+
'notes': []
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
# Calculate analytical thermal stress
|
|
484
|
+
# For a constrained plate: σ = E * α * ΔT
|
|
485
|
+
sigma_thermal = self.elastic_modulus * self.thermal_expansion * self.temperature_change
|
|
486
|
+
|
|
487
|
+
result['analytical'] = {
|
|
488
|
+
'thermal_stress': sigma_thermal,
|
|
489
|
+
'thermal_strain': self.thermal_expansion * self.temperature_change
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
result['notes'].append("Thermal load implementation not yet complete")
|
|
493
|
+
result['notes'].append("Would require: thermal strain matrix, temperature field")
|
|
494
|
+
result['notes'].append(f"Expected thermal stress: {sigma_thermal/1e6:.1f} MPa")
|
|
495
|
+
|
|
496
|
+
return result
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class TestCaseRunner:
|
|
500
|
+
"""Runner for test cases."""
|
|
501
|
+
|
|
502
|
+
TEST_CASES = {
|
|
503
|
+
'ship_panel': ShipPanelTestCase,
|
|
504
|
+
'buckling': BucklingTestCase,
|
|
505
|
+
'vibration': VibrationTestCase,
|
|
506
|
+
'thermal': ThermalLoadTestCase
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
@classmethod
|
|
510
|
+
def run_test_case(cls, test_case_name: str) -> Dict[str, Any]:
|
|
511
|
+
"""Run a specific test case."""
|
|
512
|
+
if test_case_name not in cls.TEST_CASES:
|
|
513
|
+
raise ValueError(f"Unknown test case: {test_case_name}")
|
|
514
|
+
|
|
515
|
+
test_case = cls.TEST_CASES[test_case_name]()
|
|
516
|
+
return test_case.run()
|
|
517
|
+
|
|
518
|
+
@classmethod
|
|
519
|
+
def run_all_test_cases(cls) -> Dict[str, Dict[str, Any]]:
|
|
520
|
+
"""Run all test cases."""
|
|
521
|
+
results = {}
|
|
522
|
+
|
|
523
|
+
for name, test_class in cls.TEST_CASES.items():
|
|
524
|
+
try:
|
|
525
|
+
test_case = test_class()
|
|
526
|
+
results[name] = test_case.run()
|
|
527
|
+
except Exception as e:
|
|
528
|
+
results[name] = {
|
|
529
|
+
'test_case': name,
|
|
530
|
+
'error': str(e),
|
|
531
|
+
'status': 'FAILED'
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return results
|
|
535
|
+
|
|
536
|
+
@classmethod
|
|
537
|
+
def save_test_case_results(cls, results: Dict[str, Dict[str, Any]], filename: str = "test_case_results.json"):
|
|
538
|
+
"""Save test case results to file."""
|
|
539
|
+
import json
|
|
540
|
+
|
|
541
|
+
def convert_numpy_types(obj):
|
|
542
|
+
"""Convert numpy types to native Python types for JSON serialization."""
|
|
543
|
+
if isinstance(obj, dict):
|
|
544
|
+
return {k: convert_numpy_types(v) for k, v in obj.items()}
|
|
545
|
+
elif isinstance(obj, list):
|
|
546
|
+
return [convert_numpy_types(item) for item in obj]
|
|
547
|
+
elif hasattr(obj, 'item'): # numpy scalar types (int64, float64, etc.)
|
|
548
|
+
return obj.item()
|
|
549
|
+
elif hasattr(obj, 'tolist'): # numpy arrays
|
|
550
|
+
return obj.tolist()
|
|
551
|
+
return obj
|
|
552
|
+
|
|
553
|
+
with open(filename, 'w') as f:
|
|
554
|
+
json.dump(convert_numpy_types(results), f, indent=2)
|
|
555
|
+
|
|
556
|
+
@classmethod
|
|
557
|
+
def print_test_case_summary(cls, results: Dict[str, Dict[str, Any]]):
|
|
558
|
+
"""Print summary of test case results."""
|
|
559
|
+
print("=" * 70)
|
|
560
|
+
print("TEST CASE RESULTS SUMMARY")
|
|
561
|
+
print("=" * 70)
|
|
562
|
+
|
|
563
|
+
for name, result in results.items():
|
|
564
|
+
status = result.get('status', 'COMPLETED')
|
|
565
|
+
if 'error' in result:
|
|
566
|
+
status = 'FAILED'
|
|
567
|
+
|
|
568
|
+
print(f"\n{name.upper()}")
|
|
569
|
+
print("-" * len(name))
|
|
570
|
+
print(f"Status: {status}")
|
|
571
|
+
|
|
572
|
+
if 'error' in result:
|
|
573
|
+
print(f"Error: {result['error']}")
|
|
574
|
+
elif 'results' in result:
|
|
575
|
+
if 'max_displacement' in result['results']:
|
|
576
|
+
print(f"Max Displacement: {result['results']['max_displacement']:.6e} m")
|
|
577
|
+
if 'mesh' in result['results']:
|
|
578
|
+
print(f"Mesh: {result['results']['mesh']['num_nodes']} nodes, {result['results']['mesh']['num_elements']} elements")
|
|
579
|
+
|
|
580
|
+
print("\n" + "=" * 70)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def run_ship_panel_test() -> Dict[str, Any]:
|
|
584
|
+
"""Run the ship panel test case."""
|
|
585
|
+
test_case = ShipPanelTestCase()
|
|
586
|
+
return test_case.run()
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def run_all_demo_test_cases() -> Dict[str, Dict[str, Any]]:
|
|
590
|
+
"""Run all demonstration test cases."""
|
|
591
|
+
return TestCaseRunner.run_all_test_cases()
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
if __name__ == "__main__":
|
|
595
|
+
import time
|
|
596
|
+
|
|
597
|
+
print("Running Comprehensive Test Cases...")
|
|
598
|
+
print("=" * 70)
|
|
599
|
+
|
|
600
|
+
start_time = time.time()
|
|
601
|
+
results = run_all_demo_test_cases()
|
|
602
|
+
elapsed_time = time.time() - start_time
|
|
603
|
+
|
|
604
|
+
TestCaseRunner.print_test_case_summary(results)
|
|
605
|
+
|
|
606
|
+
print(f"\nTotal time: {elapsed_time:.2f} seconds")
|
|
607
|
+
|
|
608
|
+
# Save results
|
|
609
|
+
TestCaseRunner.save_test_case_results(results, "test_case_results.json")
|
|
610
|
+
print("Results saved to test_case_results.json")
|