ANYmaterial 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.
- anymaterial/__init__.py +135 -0
- anymaterial/__main__.py +351 -0
- anymaterial/contract.py +127 -0
- anymaterial/curves.py +323 -0
- anymaterial/data/dnv_rp_c208.json +41 -0
- anymaterial/data/materials.json +490 -0
- anymaterial/gui.py +747 -0
- anymaterial/isotropic.py +76 -0
- anymaterial/library.py +550 -0
- anymaterial/orthotropic.py +94 -0
- anymaterial/plot.py +218 -0
- anymaterial/reductions.py +125 -0
- anymaterial/spec.py +321 -0
- anymaterial/validation.py +171 -0
- anymaterial/yield_criteria.py +286 -0
- anymaterial-0.1.0.dist-info/METADATA +273 -0
- anymaterial-0.1.0.dist-info/RECORD +21 -0
- anymaterial-0.1.0.dist-info/WHEEL +5 -0
- anymaterial-0.1.0.dist-info/entry_points.txt +5 -0
- anymaterial-0.1.0.dist-info/licenses/LICENSE +674 -0
- anymaterial-0.1.0.dist-info/top_level.txt +1 -0
anymaterial/__init__.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Structural material models for finite-element analysis.
|
|
2
|
+
|
|
3
|
+
The package owns material behaviour and nothing else: elastic symmetry and
|
|
4
|
+
compliance, the reductions a shell or beam formulation needs, nonlinear
|
|
5
|
+
hardening curves, directional yield criteria, and a serializable
|
|
6
|
+
specification. Meshes, sections and solvers live elsewhere.
|
|
7
|
+
|
|
8
|
+
Elastic compliance uses engineering Voigt order ``[11, 22, 33, 23, 13, 12]``:
|
|
9
|
+
|
|
10
|
+
``[eps11, eps22, eps33, gamma23, gamma13, gamma12] = S @
|
|
11
|
+
[sig11, sig22, sig33, tau23, tau13, tau12]``
|
|
12
|
+
|
|
13
|
+
This ordering is the family-wide convention. It is stated here because this
|
|
14
|
+
package defines it, and every consumer is expected to match it rather than
|
|
15
|
+
transposing at the boundary.
|
|
16
|
+
|
|
17
|
+
The package deliberately does not import ANYsolver. Materials are described
|
|
18
|
+
here and consumed there, never the other way round, so the dependency stays
|
|
19
|
+
acyclic and a material can be validated without a solver present.
|
|
20
|
+
|
|
21
|
+
``anymaterial.gui`` is not imported here, so importing the package never
|
|
22
|
+
requires a display or a tkinter build.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from .contract import (
|
|
28
|
+
ENGINEERING_VOIGT_ORDER,
|
|
29
|
+
SUPPORTED_ELASTIC_SYMMETRIES,
|
|
30
|
+
StructuralMaterial,
|
|
31
|
+
elastic_compliance_matrix,
|
|
32
|
+
is_isotropic_material,
|
|
33
|
+
is_orthotropic_material,
|
|
34
|
+
material_symmetry,
|
|
35
|
+
)
|
|
36
|
+
from .curves import (
|
|
37
|
+
DNVC208MaterialCurve,
|
|
38
|
+
HardeningCurve,
|
|
39
|
+
LinearHardeningCurve,
|
|
40
|
+
PiecewiseLinearCurve,
|
|
41
|
+
PowerLawHardeningCurve,
|
|
42
|
+
curve_from_properties,
|
|
43
|
+
)
|
|
44
|
+
from .isotropic import IsotropicMaterial
|
|
45
|
+
from .library import (
|
|
46
|
+
STEEL_DENSITY,
|
|
47
|
+
STEEL_POISSON_RATIO,
|
|
48
|
+
LibraryEntry,
|
|
49
|
+
MaterialLibrary,
|
|
50
|
+
add_to_user_library,
|
|
51
|
+
available_grades,
|
|
52
|
+
builtin_library,
|
|
53
|
+
dnv_c208_steel_curve,
|
|
54
|
+
dnv_c208_steel_properties,
|
|
55
|
+
library,
|
|
56
|
+
steel,
|
|
57
|
+
thickness_classes,
|
|
58
|
+
user_library_path,
|
|
59
|
+
)
|
|
60
|
+
from .plot import CurveSeries, curve_svg, sample_curve, write_curve_svg
|
|
61
|
+
from .orthotropic import OrthotropicMaterial
|
|
62
|
+
from .reductions import (
|
|
63
|
+
BeamMaterialProperties,
|
|
64
|
+
beam_material_properties,
|
|
65
|
+
shell_characteristic_modulus,
|
|
66
|
+
shell_material_matrices,
|
|
67
|
+
)
|
|
68
|
+
from .spec import (
|
|
69
|
+
MaterialSpec,
|
|
70
|
+
build_hardening_curve,
|
|
71
|
+
hardening_descriptor,
|
|
72
|
+
load_specs,
|
|
73
|
+
save_specs,
|
|
74
|
+
spec_from_material,
|
|
75
|
+
)
|
|
76
|
+
from .validation import material_validation_errors, validate_material
|
|
77
|
+
from .yield_criteria import (
|
|
78
|
+
Hill48Yield,
|
|
79
|
+
hill48_coefficients,
|
|
80
|
+
hill48_equivalent_stress,
|
|
81
|
+
hill48_strengths,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
__version__ = "0.1.0"
|
|
85
|
+
|
|
86
|
+
__all__ = [
|
|
87
|
+
"BeamMaterialProperties",
|
|
88
|
+
"CurveSeries",
|
|
89
|
+
"DNVC208MaterialCurve",
|
|
90
|
+
"ENGINEERING_VOIGT_ORDER",
|
|
91
|
+
"HardeningCurve",
|
|
92
|
+
"Hill48Yield",
|
|
93
|
+
"IsotropicMaterial",
|
|
94
|
+
"LibraryEntry",
|
|
95
|
+
"LinearHardeningCurve",
|
|
96
|
+
"MaterialLibrary",
|
|
97
|
+
"MaterialSpec",
|
|
98
|
+
"OrthotropicMaterial",
|
|
99
|
+
"PiecewiseLinearCurve",
|
|
100
|
+
"PowerLawHardeningCurve",
|
|
101
|
+
"STEEL_DENSITY",
|
|
102
|
+
"STEEL_POISSON_RATIO",
|
|
103
|
+
"SUPPORTED_ELASTIC_SYMMETRIES",
|
|
104
|
+
"StructuralMaterial",
|
|
105
|
+
"add_to_user_library",
|
|
106
|
+
"available_grades",
|
|
107
|
+
"beam_material_properties",
|
|
108
|
+
"build_hardening_curve",
|
|
109
|
+
"builtin_library",
|
|
110
|
+
"curve_from_properties",
|
|
111
|
+
"curve_svg",
|
|
112
|
+
"dnv_c208_steel_curve",
|
|
113
|
+
"dnv_c208_steel_properties",
|
|
114
|
+
"elastic_compliance_matrix",
|
|
115
|
+
"hardening_descriptor",
|
|
116
|
+
"hill48_coefficients",
|
|
117
|
+
"hill48_equivalent_stress",
|
|
118
|
+
"hill48_strengths",
|
|
119
|
+
"is_isotropic_material",
|
|
120
|
+
"is_orthotropic_material",
|
|
121
|
+
"library",
|
|
122
|
+
"load_specs",
|
|
123
|
+
"material_symmetry",
|
|
124
|
+
"material_validation_errors",
|
|
125
|
+
"sample_curve",
|
|
126
|
+
"save_specs",
|
|
127
|
+
"shell_characteristic_modulus",
|
|
128
|
+
"shell_material_matrices",
|
|
129
|
+
"spec_from_material",
|
|
130
|
+
"steel",
|
|
131
|
+
"thickness_classes",
|
|
132
|
+
"user_library_path",
|
|
133
|
+
"validate_material",
|
|
134
|
+
"write_curve_svg",
|
|
135
|
+
]
|
anymaterial/__main__.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Command line interface.
|
|
2
|
+
|
|
3
|
+
``python -m anymaterial <command>``, or ``anymaterial <command>`` once
|
|
4
|
+
installed. Every command takes ``--json`` for machine-readable output, so the
|
|
5
|
+
tool is usable from a script without parsing formatted text.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any, Dict, List, Sequence
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from .contract import elastic_compliance_matrix, material_symmetry
|
|
18
|
+
from .library import (
|
|
19
|
+
STATUSES,
|
|
20
|
+
LibraryEntry,
|
|
21
|
+
add_to_user_library,
|
|
22
|
+
available_grades,
|
|
23
|
+
dnv_c208_steel_curve,
|
|
24
|
+
dnv_c208_steel_properties,
|
|
25
|
+
library,
|
|
26
|
+
thickness_classes,
|
|
27
|
+
user_library_path,
|
|
28
|
+
)
|
|
29
|
+
from .plot import sample_curve, write_curve_svg
|
|
30
|
+
from .reductions import beam_material_properties, shell_material_matrices
|
|
31
|
+
from .spec import MaterialSpec, load_specs
|
|
32
|
+
from .validation import material_validation_errors
|
|
33
|
+
|
|
34
|
+
__all__ = ["main"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _print_json(payload: Any) -> None:
|
|
38
|
+
print(json.dumps(payload, indent=2, sort_keys=True, default=float))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _command_grades(args: argparse.Namespace) -> int:
|
|
42
|
+
payload = {grade: list(thickness_classes(grade)) for grade in available_grades()}
|
|
43
|
+
if args.json:
|
|
44
|
+
_print_json(payload)
|
|
45
|
+
return 0
|
|
46
|
+
print("DNV-RP-C208 low-fractile steel grades (thickness in mm):")
|
|
47
|
+
for grade, classes in payload.items():
|
|
48
|
+
print(f" {grade}: {', '.join(classes)}")
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _command_properties(args: argparse.Namespace) -> int:
|
|
53
|
+
properties = dnv_c208_steel_properties(
|
|
54
|
+
args.grade, args.thickness, thickness_class=args.thickness_class
|
|
55
|
+
)
|
|
56
|
+
if args.json:
|
|
57
|
+
_print_json(properties)
|
|
58
|
+
return 0
|
|
59
|
+
print(f"{properties['grade']} thickness {properties['thickness_mm']:g} mm "
|
|
60
|
+
f"class {properties['thickness_class']}")
|
|
61
|
+
print(f" source {properties['source']}")
|
|
62
|
+
print(f" E {float(properties['E_pa']) / 1.0e9:.1f} GPa")
|
|
63
|
+
for key in ("sigma_prop", "sigma_yield", "sigma_yield_2", "K"):
|
|
64
|
+
print(f" {key:<15} {float(properties[key]) / 1.0e6:.1f} MPa")
|
|
65
|
+
for key in ("eps_p_y1", "eps_p_y2", "n"):
|
|
66
|
+
print(f" {key:<15} {float(properties[key]):g}")
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _command_curve(args: argparse.Namespace) -> int:
|
|
71
|
+
curve = dnv_c208_steel_curve(args.grade, args.thickness)
|
|
72
|
+
strains = np.linspace(0.0, float(args.max_strain), int(args.points))
|
|
73
|
+
stresses = curve.flow_stress(strains)
|
|
74
|
+
moduli = curve.hardening_modulus(strains)
|
|
75
|
+
if args.json:
|
|
76
|
+
_print_json(
|
|
77
|
+
{
|
|
78
|
+
"grade": args.grade.upper(),
|
|
79
|
+
"thickness_m": float(args.thickness),
|
|
80
|
+
"parameters": curve.as_dict(),
|
|
81
|
+
"plastic_strain": [float(value) for value in strains],
|
|
82
|
+
"flow_stress_pa": [float(value) for value in stresses],
|
|
83
|
+
"hardening_modulus_pa": [float(value) for value in moduli],
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
return 0
|
|
87
|
+
print(f"{'eps_p':>10} {'sigma [MPa]':>14} {'H [MPa]':>14}")
|
|
88
|
+
for strain, stress, modulus in zip(strains, stresses, moduli):
|
|
89
|
+
print(f"{strain:>10.5f} {stress / 1.0e6:>14.2f} {modulus / 1.0e6:>14.2f}")
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _describe(spec: MaterialSpec) -> Dict[str, Any]:
|
|
94
|
+
"""Summarize one specification, including its derived reductions."""
|
|
95
|
+
|
|
96
|
+
material = spec.build()
|
|
97
|
+
compliance = elastic_compliance_matrix(material)
|
|
98
|
+
plane_stress, transverse_shear, drilling = shell_material_matrices(material)
|
|
99
|
+
beam = beam_material_properties(material)
|
|
100
|
+
return {
|
|
101
|
+
"name": spec.name,
|
|
102
|
+
"symmetry": material_symmetry(material),
|
|
103
|
+
"density": float(spec.density),
|
|
104
|
+
"yield_stress": float(spec.yield_stress),
|
|
105
|
+
"nonlinear": spec.is_nonlinear,
|
|
106
|
+
"hardening": dict(spec.hardening) if spec.hardening else None,
|
|
107
|
+
"constants": dict(spec.constants),
|
|
108
|
+
"compliance_diagonal": [float(value) for value in np.diag(compliance)],
|
|
109
|
+
"shell_plane_stress": [[float(value) for value in row] for row in plane_stress],
|
|
110
|
+
"shell_transverse_shear": [float(value) for value in np.diag(transverse_shear)],
|
|
111
|
+
"shell_drilling_shear": float(drilling),
|
|
112
|
+
"beam": {
|
|
113
|
+
"axial_modulus": beam.axial_modulus,
|
|
114
|
+
"shear_modulus_xy": beam.shear_modulus_xy,
|
|
115
|
+
"shear_modulus_xz": beam.shear_modulus_xz,
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _command_library(args: argparse.Namespace) -> int:
|
|
121
|
+
entries = library().find(
|
|
122
|
+
category=args.category, status=args.status, text=args.search,
|
|
123
|
+
nonlinear=True if args.nonlinear else None,
|
|
124
|
+
)
|
|
125
|
+
if args.json:
|
|
126
|
+
_print_json({
|
|
127
|
+
"user_library": str(user_library_path()),
|
|
128
|
+
"materials": [entry.to_dict() for entry in entries],
|
|
129
|
+
})
|
|
130
|
+
return 0
|
|
131
|
+
if not entries:
|
|
132
|
+
print("no materials match")
|
|
133
|
+
return 0
|
|
134
|
+
# Sized to the content: a fixed width truncated the longer alloy names into
|
|
135
|
+
# ambiguity, which is the one thing a material listing must not do.
|
|
136
|
+
name_width = max(len("material"), *(len(entry.name) for entry in entries))
|
|
137
|
+
category_width = max(len("category"), *(len(entry.category) for entry in entries))
|
|
138
|
+
print(
|
|
139
|
+
f"{'material':<{name_width}} {'category':<{category_width}} "
|
|
140
|
+
f"{'status':<10} {'yield':>9} hardening"
|
|
141
|
+
)
|
|
142
|
+
for entry in entries:
|
|
143
|
+
yield_mpa = f"{entry.spec.yield_stress / 1.0e6:.0f} MPa" if entry.spec.yield_stress else "-"
|
|
144
|
+
hardening = entry.spec.hardening["kind"] if entry.spec.hardening else "elastic"
|
|
145
|
+
print(
|
|
146
|
+
f"{entry.name:<{name_width}} {entry.category:<{category_width}} "
|
|
147
|
+
f"{entry.status:<10} {yield_mpa:>9} {hardening}"
|
|
148
|
+
)
|
|
149
|
+
print()
|
|
150
|
+
print(f"user library: {user_library_path()}")
|
|
151
|
+
# Said every time the list is printed, because the whole risk this library
|
|
152
|
+
# carries is a looked-up number being used as a design value. Only shown for
|
|
153
|
+
# the statuses actually listed, so the warning stays worth reading.
|
|
154
|
+
shown = {entry.status for entry in entries}
|
|
155
|
+
if "measured" in shown:
|
|
156
|
+
print("status 'measured' is the MEAN of a test campaign, NOT a characteristic value:")
|
|
157
|
+
print("the mean yield exceeds the nominal, so using it as a design strength is")
|
|
158
|
+
print("unconservative. Use it to validate a model against the tests it came from.")
|
|
159
|
+
if "indicative" in shown:
|
|
160
|
+
print("status 'indicative' means a typical published figure, not a design value:")
|
|
161
|
+
print("check it against the governing standard or the mill certificate.")
|
|
162
|
+
if shown <= {"tabulated"}:
|
|
163
|
+
print("all listed materials are 'tabulated': reproduced from a standard's own table.")
|
|
164
|
+
return 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _command_plot(args: argparse.Namespace) -> int:
|
|
168
|
+
catalogue = library()
|
|
169
|
+
series = []
|
|
170
|
+
for name in args.materials:
|
|
171
|
+
entry = catalogue.get(name)
|
|
172
|
+
curve = entry.spec.hardening_curve()
|
|
173
|
+
if curve is None:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
f"{name!r} is elastic and has no flow curve to plot; "
|
|
176
|
+
"list the nonlinear materials with `anymaterial library --nonlinear`"
|
|
177
|
+
)
|
|
178
|
+
series.append(sample_curve(curve, name, max_strain=args.max_strain, samples=args.points))
|
|
179
|
+
|
|
180
|
+
path = write_curve_svg(
|
|
181
|
+
args.output, series, overwrite=args.overwrite, title=args.title, stress_unit="MPa"
|
|
182
|
+
)
|
|
183
|
+
if args.json:
|
|
184
|
+
_print_json({"output": str(path), "materials": list(args.materials)})
|
|
185
|
+
else:
|
|
186
|
+
print(f"wrote {path} ({len(series)} curve(s))")
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _command_add(args: argparse.Namespace) -> int:
|
|
191
|
+
hardening = None
|
|
192
|
+
if args.grade:
|
|
193
|
+
hardening = {"kind": "dnv_c208", "grade": args.grade, "thickness": args.thickness}
|
|
194
|
+
elif args.hardening_modulus is not None:
|
|
195
|
+
hardening = {
|
|
196
|
+
"kind": "linear",
|
|
197
|
+
"sigma_yield": args.yield_stress * 1.0e6,
|
|
198
|
+
"hardening_modulus": args.hardening_modulus * 1.0e6,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
entry = LibraryEntry(
|
|
202
|
+
spec=MaterialSpec(
|
|
203
|
+
name=args.name,
|
|
204
|
+
symmetry="isotropic",
|
|
205
|
+
constants={
|
|
206
|
+
"elastic_modulus": args.elastic_modulus * 1.0e9,
|
|
207
|
+
"poisson_ratio": args.poisson_ratio,
|
|
208
|
+
},
|
|
209
|
+
density=args.density,
|
|
210
|
+
yield_stress=args.yield_stress * 1.0e6,
|
|
211
|
+
hardening=hardening,
|
|
212
|
+
),
|
|
213
|
+
category=args.category,
|
|
214
|
+
status="user",
|
|
215
|
+
standard=args.standard,
|
|
216
|
+
source=args.source,
|
|
217
|
+
notes=args.notes,
|
|
218
|
+
)
|
|
219
|
+
path = add_to_user_library(entry, replace_existing=args.replace)
|
|
220
|
+
if args.json:
|
|
221
|
+
_print_json({"added": entry.name, "library": str(path)})
|
|
222
|
+
else:
|
|
223
|
+
print(f"added {entry.name!r} to {path}")
|
|
224
|
+
return 0
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _command_show(args: argparse.Namespace) -> int:
|
|
228
|
+
summaries = [_describe(spec) for spec in load_specs(args.input)]
|
|
229
|
+
if args.json:
|
|
230
|
+
_print_json({"materials": summaries})
|
|
231
|
+
return 0
|
|
232
|
+
for summary in summaries:
|
|
233
|
+
print(f"{summary['name']} ({summary['symmetry']})")
|
|
234
|
+
print(f" density {summary['density']:g} kg/m3")
|
|
235
|
+
print(f" yield stress {summary['yield_stress'] / 1.0e6:g} MPa")
|
|
236
|
+
print(f" nonlinear {summary['nonlinear']}")
|
|
237
|
+
if summary["hardening"]:
|
|
238
|
+
print(f" hardening {summary['hardening']}")
|
|
239
|
+
for key, value in sorted(summary["constants"].items()):
|
|
240
|
+
print(f" {key:<15} {value:g}")
|
|
241
|
+
print(f" beam E {summary['beam']['axial_modulus'] / 1.0e9:.2f} GPa")
|
|
242
|
+
print(f" drilling G {summary['shell_drilling_shear'] / 1.0e9:.2f} GPa")
|
|
243
|
+
return 0
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _command_validate(args: argparse.Namespace) -> int:
|
|
247
|
+
results: List[Dict[str, Any]] = []
|
|
248
|
+
exit_code = 0
|
|
249
|
+
for spec in load_specs(args.input):
|
|
250
|
+
try:
|
|
251
|
+
material = spec.build()
|
|
252
|
+
except ValueError as error:
|
|
253
|
+
results.append({"name": spec.name, "valid": False, "errors": [str(error)]})
|
|
254
|
+
exit_code = 1
|
|
255
|
+
continue
|
|
256
|
+
errors = list(material_validation_errors(material))
|
|
257
|
+
results.append({"name": spec.name, "valid": not errors, "errors": errors})
|
|
258
|
+
if errors:
|
|
259
|
+
exit_code = 1
|
|
260
|
+
|
|
261
|
+
if args.json:
|
|
262
|
+
_print_json({"materials": results})
|
|
263
|
+
return exit_code
|
|
264
|
+
for result in results:
|
|
265
|
+
status = "ok" if result["valid"] else "INVALID"
|
|
266
|
+
print(f"{result['name']}: {status}")
|
|
267
|
+
for message in result["errors"]:
|
|
268
|
+
print(f" - {message}")
|
|
269
|
+
return exit_code
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
273
|
+
parser = argparse.ArgumentParser(prog="anymaterial", description=__doc__.splitlines()[0])
|
|
274
|
+
parser.add_argument("--json", action="store_true", help="print machine-readable JSON")
|
|
275
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
276
|
+
|
|
277
|
+
sub.add_parser("grades", help="list tabulated steel grades and thickness classes")
|
|
278
|
+
|
|
279
|
+
catalogue = sub.add_parser("library", help="list the materials in the library")
|
|
280
|
+
catalogue.add_argument("--category", help="filter by category, e.g. aluminium")
|
|
281
|
+
# Derived from the library's own list, so the two cannot drift apart.
|
|
282
|
+
catalogue.add_argument("--status", choices=STATUSES)
|
|
283
|
+
catalogue.add_argument("--search", help="filter by name or category substring")
|
|
284
|
+
catalogue.add_argument("--nonlinear", action="store_true", help="only materials with a flow curve")
|
|
285
|
+
|
|
286
|
+
plot = sub.add_parser("plot", help="plot flow curves to an SVG file")
|
|
287
|
+
plot.add_argument("materials", nargs="+", help="library material names")
|
|
288
|
+
plot.add_argument("--output", "-o", default="flow_curves.svg")
|
|
289
|
+
plot.add_argument("--title", default="Flow curves")
|
|
290
|
+
plot.add_argument("--max-strain", type=float, default=0.10)
|
|
291
|
+
plot.add_argument("--points", type=int, default=201)
|
|
292
|
+
plot.add_argument("--overwrite", action="store_true")
|
|
293
|
+
|
|
294
|
+
add = sub.add_parser("add", help="add an isotropic material to the user library")
|
|
295
|
+
add.add_argument("name")
|
|
296
|
+
add.add_argument("--elastic-modulus", type=float, required=True, help="E in GPa")
|
|
297
|
+
add.add_argument("--poisson-ratio", type=float, default=0.3)
|
|
298
|
+
add.add_argument("--density", type=float, default=7850.0, help="kg/m3")
|
|
299
|
+
add.add_argument("--yield-stress", type=float, default=0.0, help="MPa")
|
|
300
|
+
add.add_argument("--hardening-modulus", type=float, help="bilinear plastic modulus in MPa")
|
|
301
|
+
add.add_argument("--grade", help="attach a DNV-RP-C208 curve for this grade instead")
|
|
302
|
+
add.add_argument("--thickness", type=float, default=0.010, help="metres, with --grade")
|
|
303
|
+
add.add_argument("--category", default="other")
|
|
304
|
+
add.add_argument("--standard", help="the standard the numbers come from")
|
|
305
|
+
add.add_argument("--source", help="where the numbers came from")
|
|
306
|
+
add.add_argument("--notes")
|
|
307
|
+
add.add_argument("--replace", action="store_true", help="overwrite a material of the same name")
|
|
308
|
+
|
|
309
|
+
properties = sub.add_parser("properties", help="show one RP-C208 table row in SI units")
|
|
310
|
+
properties.add_argument("grade")
|
|
311
|
+
properties.add_argument("thickness", type=float, help="plate thickness in metres")
|
|
312
|
+
properties.add_argument(
|
|
313
|
+
"--thickness-class",
|
|
314
|
+
default="auto",
|
|
315
|
+
help="select a table row explicitly, e.g. '40 < t <= 63'",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
curve = sub.add_parser("curve", help="sample a flow curve")
|
|
319
|
+
curve.add_argument("grade")
|
|
320
|
+
curve.add_argument("thickness", type=float, help="plate thickness in metres")
|
|
321
|
+
curve.add_argument("--points", type=int, default=11)
|
|
322
|
+
curve.add_argument("--max-strain", type=float, default=0.1)
|
|
323
|
+
|
|
324
|
+
show = sub.add_parser("show", help="summarize materials in a JSON file")
|
|
325
|
+
show.add_argument("input")
|
|
326
|
+
|
|
327
|
+
validate = sub.add_parser("validate", help="validate materials in a JSON file")
|
|
328
|
+
validate.add_argument("input")
|
|
329
|
+
|
|
330
|
+
args = parser.parse_args(argv)
|
|
331
|
+
handlers = {
|
|
332
|
+
"grades": _command_grades,
|
|
333
|
+
"library": _command_library,
|
|
334
|
+
"plot": _command_plot,
|
|
335
|
+
"add": _command_add,
|
|
336
|
+
"properties": _command_properties,
|
|
337
|
+
"curve": _command_curve,
|
|
338
|
+
"show": _command_show,
|
|
339
|
+
"validate": _command_validate,
|
|
340
|
+
}
|
|
341
|
+
try:
|
|
342
|
+
return handlers[args.command](args)
|
|
343
|
+
except (FileExistsError, FileNotFoundError, KeyError, NotImplementedError, ValueError) as error:
|
|
344
|
+
# A bad grade, an out-of-range thickness or a malformed file is a usage
|
|
345
|
+
# error, not a crash: report it on stderr and exit non-zero.
|
|
346
|
+
print(f"error: {error}", file=sys.stderr)
|
|
347
|
+
return 2
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if __name__ == "__main__": # pragma: no cover - process entry point
|
|
351
|
+
raise SystemExit(main())
|
anymaterial/contract.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""The structural material contract.
|
|
2
|
+
|
|
3
|
+
A consumer needs four things from a material: a name, a density, a declared
|
|
4
|
+
elastic symmetry, and a 6x6 engineering compliance. Everything else -- shell
|
|
5
|
+
plane-stress matrices, beam axial and shear constants, yield surfaces -- is
|
|
6
|
+
derived from those, in :mod:`anymaterial.reductions` and elsewhere.
|
|
7
|
+
|
|
8
|
+
The contract is a :class:`typing.Protocol` rather than a base class on purpose.
|
|
9
|
+
A solver should accept a material without importing the class that defines it,
|
|
10
|
+
which is what allows this package and its consumers to be released
|
|
11
|
+
independently. Validation is therefore written against attributes, not types.
|
|
12
|
+
|
|
13
|
+
Voigt order is engineering ``[11, 22, 33, 23, 13, 12]``:
|
|
14
|
+
|
|
15
|
+
``[eps11, eps22, eps33, gamma23, gamma13, gamma12] = S @
|
|
16
|
+
[sig11, sig22, sig33, tau23, tau13, tau12]``
|
|
17
|
+
|
|
18
|
+
Shear strains are engineering angles, twice the tensor components. Getting
|
|
19
|
+
this wrong scales every shear term by two and is not detectable from the
|
|
20
|
+
diagonal, so the order is stated once here and matched everywhere.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any, Protocol, Tuple, runtime_checkable
|
|
26
|
+
|
|
27
|
+
import numpy as np
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"ENGINEERING_VOIGT_ORDER",
|
|
31
|
+
"SUPPORTED_ELASTIC_SYMMETRIES",
|
|
32
|
+
"StructuralMaterial",
|
|
33
|
+
"elastic_compliance_matrix",
|
|
34
|
+
"is_isotropic_material",
|
|
35
|
+
"is_orthotropic_material",
|
|
36
|
+
"material_symmetry",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
ENGINEERING_VOIGT_ORDER: Tuple[str, ...] = ("11", "22", "33", "23", "13", "12")
|
|
41
|
+
|
|
42
|
+
SUPPORTED_ELASTIC_SYMMETRIES = frozenset({"isotropic", "orthotropic"})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@runtime_checkable
|
|
46
|
+
class StructuralMaterial(Protocol):
|
|
47
|
+
"""Minimal contract for a structural material."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
density: float
|
|
51
|
+
elastic_symmetry: str
|
|
52
|
+
|
|
53
|
+
def elastic_compliance_matrix(self) -> np.ndarray:
|
|
54
|
+
"""Return the 6x6 engineering compliance in Voigt order."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def material_symmetry(material: Any) -> str:
|
|
58
|
+
"""Return the normalized declared elastic symmetry.
|
|
59
|
+
|
|
60
|
+
Duck-typed isotropic objects that predate the declaration stay recognizable
|
|
61
|
+
when they expose the historical ``elastic_modulus`` and ``poisson_ratio``
|
|
62
|
+
fields. That fallback exists for material records arriving from older
|
|
63
|
+
callers and deserializers, not as an alternative to declaring the symmetry.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
symmetry = getattr(material, "elastic_symmetry", None)
|
|
67
|
+
if symmetry is None and hasattr(material, "elastic_modulus") and hasattr(material, "poisson_ratio"):
|
|
68
|
+
return "isotropic"
|
|
69
|
+
if not isinstance(symmetry, str) or not symmetry.strip():
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"Structural material must declare elastic_symmetry as 'isotropic' or 'orthotropic'"
|
|
72
|
+
)
|
|
73
|
+
return symmetry.strip().lower()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def is_isotropic_material(material: Any) -> bool:
|
|
77
|
+
"""Return whether a material declares isotropic elasticity."""
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
return material_symmetry(material) == "isotropic"
|
|
81
|
+
except ValueError:
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_orthotropic_material(material: Any) -> bool:
|
|
86
|
+
"""Return whether a material declares orthotropic elasticity."""
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
return material_symmetry(material) == "orthotropic"
|
|
90
|
+
except ValueError:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def elastic_compliance_matrix(material: Any) -> np.ndarray:
|
|
95
|
+
"""Return a material's compliance with a stable numpy representation.
|
|
96
|
+
|
|
97
|
+
A material that provides ``elastic_compliance_matrix()`` is asked for it.
|
|
98
|
+
A legacy isotropic record that does not is assembled from ``E`` and ``nu``,
|
|
99
|
+
so the two reach every reduction by the same path and there is one place
|
|
100
|
+
for the algebra to be wrong.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
provider = getattr(material, "elastic_compliance_matrix", None)
|
|
104
|
+
if callable(provider):
|
|
105
|
+
matrix = np.asarray(provider(), dtype=float)
|
|
106
|
+
elif is_isotropic_material(material):
|
|
107
|
+
E = float(material.elastic_modulus)
|
|
108
|
+
nu = float(material.poisson_ratio)
|
|
109
|
+
G = E / (2.0 * (1.0 + nu))
|
|
110
|
+
matrix = np.array(
|
|
111
|
+
[
|
|
112
|
+
[1.0 / E, -nu / E, -nu / E, 0.0, 0.0, 0.0],
|
|
113
|
+
[-nu / E, 1.0 / E, -nu / E, 0.0, 0.0, 0.0],
|
|
114
|
+
[-nu / E, -nu / E, 1.0 / E, 0.0, 0.0, 0.0],
|
|
115
|
+
[0.0, 0.0, 0.0, 1.0 / G, 0.0, 0.0],
|
|
116
|
+
[0.0, 0.0, 0.0, 0.0, 1.0 / G, 0.0],
|
|
117
|
+
[0.0, 0.0, 0.0, 0.0, 0.0, 1.0 / G],
|
|
118
|
+
],
|
|
119
|
+
dtype=float,
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
raise ValueError("Structural material must provide elastic_compliance_matrix()")
|
|
123
|
+
if matrix.shape != (6, 6):
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"Material elastic compliance must have shape (6, 6), received {matrix.shape}"
|
|
126
|
+
)
|
|
127
|
+
return matrix
|