Skip to content

Commit da3a7ee

Browse files
author
Garrett Barter
authored
Merge pull request #42 from NREL/dnv_units
Add units to DNV S-N curves
2 parents f478e52 + 9a64a50 commit da3a7ee

6 files changed

Lines changed: 133 additions & 34 deletions

File tree

.github/workflows/tests.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ jobs:
1515
fail-fast: False
1616
matrix:
1717
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
18-
python-version: ["3.11", "3.12", "3.13"]
18+
python-version: ["3.11", "3.12", "3.13", "3.14"]
1919

2020
steps:
2121
- name: checkout repository
22-
uses: actions/checkout@v5
22+
uses: actions/checkout@v6
2323

2424
- name: Set up Python ${{ matrix.python-version }}
2525
uses: actions/setup-python@v6
@@ -59,11 +59,11 @@ jobs:
5959
fail-fast: false #true
6060
matrix:
6161
os: ["ubuntu-latest", "macOS-latest", "windows-latest"]
62-
python-version: ["3.11", "3.12", "3.13"]
62+
python-version: ["3.11", "3.12", "3.13", "3.14"]
6363

6464
steps:
6565
- name: checkout repository
66-
uses: actions/checkout@v5
66+
uses: actions/checkout@v6
6767

6868
- uses: conda-incubator/setup-miniconda@v3
6969
# https://github.com/marketplace/actions/setup-miniconda

examples/aeroelastic_output_example.ipynb

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2232,15 +2232,18 @@
22322232
"There are three ways to initialize a FatigueParams instance with an associated S-N curve in the `fatpack` library:\n",
22332233
" \n",
22342234
"1. Specify a curve from DNV-RP-C203, Fatigue in Offshore Steel Structures.\n",
2235-
" Input keywords are `dnv_type` = (one of) ['air', 'seawater', 'cathodic'] and\n",
2236-
" `dnv_name` = (one of) [B1, B2, C, C1, C2, D, E, G F1, F3, G, W1, W2, W3]\n",
2235+
" Input keywords are `dnv_type` = (one of) ['air', 'seawater', 'cathodic'],\n",
2236+
" `dnv_name` = (one of) [B1, B2, C, C1, C2, D, E, G F1, F3, G, W1, W2, W3], and\n",
2237+
" `units` = (such as) 'kPa' or 'MNm' to set the input units.\n",
22372238
"\n",
2238-
"2. Specify the slope of the S-N curve and a point on the curve.\n",
2239+
"3. Specify the slope of the S-N curve and a point on the curve.\n",
22392240
" Required keywords are `slope`, `Nc` and `Sc`. Assumes a linear S-N curve.\n",
2241+
" Units are left to the user but must be consistent for all inputs.\n",
22402242
"\n",
2241-
"3. Specify the slope and the S-intercept point assuming a perflectly linear S-N curve\n",
2243+
"5. Specify the slope and the S-intercept point assuming a perflectly linear S-N curve\n",
22422244
" (which might not be the actual ultimate failure stress of the material.\n",
2243-
" Required keywords are `slope` and `S_intercept`.\n"
2245+
" Required keywords are `slope` and `S_intercept`.\n",
2246+
" Units are left to the user but must be consistent for all inputs.\n"
22442247
]
22452248
},
22462249
{
@@ -2250,11 +2253,11 @@
22502253
"outputs": [],
22512254
"source": [
22522255
"# Setting with curves found in Tables 2-1, 2-2, 2-4 from DNV-RP-C203 - Edition October 2024\n",
2253-
"myparam1 = FatigueParams(dnv_type='Air', dnv_name='D')\n",
2254-
"myparam2 = FatigueParams(load2stress=25.0, dnv_type='sea', dnv_name='c1')\n",
2256+
"myparam1 = FatigueParams(dnv_type='Air', dnv_name='D', units='kPa')\n",
2257+
"myparam2 = FatigueParams(load2stress=25.0, dnv_type='sea', dnv_name='c1', units='MPa')\n",
22552258
"\n",
22562259
"# Also showing the other available keywords\n",
2257-
"myparam3 = FatigueParams(bins=256, goodman=True, ultimate_stress=1e6, dnv_type='cathodic', dnv_name='B2')\n",
2260+
"myparam3 = FatigueParams(bins=256, goodman=True, ultimate_stress=1e6, dnv_type='cathodic', dnv_name='B2', units='N')\n",
22582261
"\n",
22592262
"# Setting with slope and a known point, (Nc, Sc)\n",
22602263
"myparam4 = FatigueParams(Sc=2e7, Nc=2e6, slope=3)\n",
@@ -2852,7 +2855,7 @@
28522855
"name": "python",
28532856
"nbconvert_exporter": "python",
28542857
"pygments_lexer": "ipython3",
2855-
"version": "3.12.9"
2858+
"version": "3.13.11"
28562859
}
28572860
},
28582861
"nbformat": 4,

pCrunch/aeroelastic_output.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def chan_idx(self, chan):
111111
raise IndexError(f"Channel '{chan}' not found.")
112112
return idx
113113

114-
def set_data(self, datain, channelsin=None):
114+
def set_data(self, datain, channelsin=None, dropna=True):
115115
if datain is None:
116116
return
117117

@@ -136,6 +136,9 @@ def set_data(self, datain, channelsin=None):
136136
#print("Unknown data type. Expected dict or list or DataFrame or Numpy Array")
137137
#print(f"Instead found, {type(datain)}")
138138

139+
if dropna:
140+
self.data = self.data[~np.isnan(self.data).any(axis=1),:]
141+
139142
def drop_channel(self, pattern):
140143
"""
141144
Drop channel based on a string pattern

pCrunch/fatigue.py

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,26 @@
5757
W3= dict(m=3.0, loga=10.493),
5858
)
5959

60-
def dnv_in_air(name):
60+
def units2nref(units):
61+
if not isinstance(units, str):
62+
raise ValueError(f"Units input must be a string. Instead got, {units}")
63+
if not (units.find("N") >= 0 or units.find("Pa") >= 0):
64+
raise ValueError(f"Units input must be in SI units of Newtons or Newton-meters or Pascals. Instead got, {units}")
65+
66+
# Get multiplier to convert from MPa or MN or MNm to x
67+
pfx = units[0]
68+
if pfx == 'k':
69+
nref = 1e3
70+
elif pfx == 'M':
71+
nref = 1
72+
elif pfx in ['N','P']:
73+
nref = 1e6
74+
else:
75+
raise ValueError(f"Unrecognized units string. Expected something like kN or MPa. Instead got, {units}")
76+
77+
return nref
78+
79+
def dnv_in_air(name, units):
6180
"""Returns a DNV endurance curve (SN curve)
6281
6382
This method returns an endurance curve in air according to
@@ -67,6 +86,9 @@ def dnv_in_air(name):
6786
---------
6887
name : str
6988
Name of the endurance curve.
89+
units : str
90+
Units for the stresses that are used for fatigue damage calculation (or forces for damage-equivalent loads).
91+
Must be SI units of Newtons or Pascals. Examples are 'kPa' or 'MNm'.
7092
7193
Returns
7294
-------
@@ -81,7 +103,8 @@ def dnv_in_air(name):
81103
"""
82104

83105
data = curves_in_air[name]
84-
curve = fatpack.BiLinearEnduranceCurve(1e6) # 1e6 for MPa to Pa
106+
nref = units2nref(units)
107+
curve = fatpack.BiLinearEnduranceCurve(nref) # 1e6 for MPa to Pa
85108
curve.Nc = 10 ** data["loga1"]
86109
curve.Nd = data["Nd"]
87110
curve.m1 = data["m1"]
@@ -90,7 +113,7 @@ def dnv_in_air(name):
90113
return curve
91114

92115

93-
def dnv_in_seawater_cathodic(name):
116+
def dnv_in_seawater_cathodic(name, units):
94117
"""Returns a DNV endurance curve (SN curve)
95118
96119
This method returns an endurance curve in seawater with
@@ -100,6 +123,9 @@ def dnv_in_seawater_cathodic(name):
100123
---------
101124
name : str
102125
Name of the endurance curve.
126+
units : str
127+
Units for the stresses that are used for fatigue damage calculation (or forces for damage-equivalent loads).
128+
Must be SI units of Newtons or Pascals. Examples are 'kPa' or 'MNm'.
103129
104130
Returns
105131
-------
@@ -113,7 +139,8 @@ def dnv_in_seawater_cathodic(name):
113139
114140
"""
115141
data = curves_in_seawater_with_cathodic_protection[name]
116-
curve = fatpack.BiLinearEnduranceCurve(1e6) # 1e6 for MPa to Pa
142+
nref = units2nref(units)
143+
curve = fatpack.BiLinearEnduranceCurve(nref) # 1e6 for MPa to Pa
117144
curve.Nc = 10 ** data["loga1"]
118145
curve.Nd = data["Nd"]
119146
curve.m1 = data["m1"]
@@ -122,7 +149,7 @@ def dnv_in_seawater_cathodic(name):
122149
curve.reference = ref
123150
return curve
124151

125-
def dnv_in_seawater(name):
152+
def dnv_in_seawater(name, units):
126153
"""Returns a DNV endurance curve (SN curve)
127154
128155
This method returns an endurance curve in seawater for
@@ -132,6 +159,9 @@ def dnv_in_seawater(name):
132159
---------
133160
name : str
134161
Name of the endurance curve.
162+
units : str
163+
Units for the stresses that are used for fatigue damage calculation (or forces for damage-equivalent loads).
164+
Must be SI units of Newtons or Pascals. Examples are 'kPa' or 'MNm'.
135165
136166
Returns
137167
-------
@@ -145,7 +175,8 @@ def dnv_in_seawater(name):
145175
146176
"""
147177
data = curves_in_seawater_for_free_corrosion[name]
148-
curve = fatpack.LinearEnduranceCurve(1e6) # 1e6 for MPa to Pa
178+
nref = units2nref(units)
179+
curve = fatpack.LinearEnduranceCurve(nref) # 1e6 for MPa to Pa
149180
curve.Nc = 10 ** data["loga"]
150181
curve.m = data["m"]
151182
ref = curves_in_seawater_for_free_corrosion["reference"]
@@ -164,13 +195,16 @@ def __init__(self, **kwargs):
164195
1. Specify a curve from DNV-RP-C203, Fatigue in Offshore Steel Structures.
165196
Input keywords are "dnv_type" = (one of) ['air', 'seawater', 'cathodic'] and
166197
"dnv_name" = (one of) [B1, B2, C, C1, C2, D, E, G F1, F3, G, W1, W2, W3]
198+
Units must be specified with the "units" input string for consistency.
167199
168200
2. Specify the slope of the S-N curve and a point on the curve.
169201
Required keywords are "slope", "Nc" and "Sc". Assumes a linear S-N curve.
202+
Units are left to the user but must be consistent for all inputs.
170203
171204
3. Specify the slope and the S-intercept point assuming a perflectly linear S-N curve
172205
(which might not be the actual ultimate failure stress of the material.
173206
Required keywords are "slope" and "S_intercept".
207+
Units are left to the user but must be consistent for all inputs.
174208
175209
Parameters
176210
----------
@@ -184,6 +218,9 @@ def __init__(self, **kwargs):
184218
dnv_name : string (optional)
185219
Grade of metal and hot spot exposure to use: [B1, B2, C, C1, C2, D, E, G F1, F3, G, W1, W2, W3]. Must also specify "dnv_type"
186220
From DNV-RP-C203, Fatigue of Metal Structures, - Edition October 2024
221+
units : string (optional)
222+
Units for the stresses that are used for fatigue damage calculation (or forces for damage-equivalent loads).
223+
Must be SI units of Newtons or Pascals. Examples are 'kPa' or 'MNm'.
187224
slope : float (optional)
188225
Wohler exponent in the traditional SN-curve of S = A * N ^ -(1/m). Must either specify Sc-Nc or S_intercept.
189226
Sc : float (optional)
@@ -205,6 +242,7 @@ def __init__(self, **kwargs):
205242
self.load2stress = kwargs.get("load2stress", 1.0)
206243
dnv_name = kwargs.get("dnv_name", "").upper()
207244
dnv_type = kwargs.get("dnv_type", "").lower()
245+
units = kwargs.get("units", "N")
208246
slope = np.abs( kwargs.get("slope", 4.0) )
209247
Sc = kwargs.get("Sc", None)
210248
Nc = kwargs.get("Nc", None)
@@ -217,19 +255,19 @@ def __init__(self, **kwargs):
217255
self.goodman = kwargs.get("goodman", self.goodman)
218256

219257
for k in kwargs:
220-
if k not in ["load2stress", "dnv_name", "dnv_type",
258+
if k not in ["load2stress", "dnv_name", "dnv_type", "units",
221259
"slope", "Sc", "Nc", "ultimate_stress",
222260
"S_intercept", "rainflow_bins", "bins",
223261
"goodman_correction", "goodman"]:
224262
print(f"Unknown keyword argument, {k}")
225263

226264
if dnv_name is not None and len(dnv_name) > 0:
227265
if dnv_type.find("cath") >= 0:
228-
self.curve = dnv_in_seawater_cathodic(dnv_name)
266+
self.curve = dnv_in_seawater_cathodic(dnv_name, units)
229267
elif dnv_type.find("sea") >= 0:
230-
self.curve = dnv_in_seawater(dnv_name)
268+
self.curve = dnv_in_seawater(dnv_name, units)
231269
elif dnv_type.find("air") >= 0:
232-
self.curve = dnv_in_air(dnv_name)
270+
self.curve = dnv_in_air(dnv_name, units)
233271
else:
234272
raise ValueError(f'Unknown DNV RP-C203 curve type, {dnv_type}. Expected [air, seawater, or cathodic]')
235273

@@ -316,6 +354,9 @@ def get_rainflow_counts(self, chan, bins, S_ult=None, goodman=False):
316354

317355
if S_ult == 0.0:
318356
raise ValueError('Must specify an ultimate_stress to use Goodman correction')
357+
358+
if np.any(Mrf > S_ult):
359+
print("Warning: Mean stress in Goodman correction is greater than ultimate stress. Will likely lean to innacurate DEL and Damage calculations.")
319360

320361
ranges = fatpack.find_goodman_equivalent_stress(ranges, Mrf, S_ult)
321362

@@ -375,6 +416,7 @@ def compute_del(self, chan, elapsed_time, **kwargs):
375416
slope = self.curve.m1 if hasattr(self.curve, 'm1') else self.curve.m
376417
DELs = Frf ** slope * Nrf / elapsed_time
377418
DEL = DELs.sum() ** (1.0 / slope)
419+
378420
# With fatpack do:
379421
#curve = fatpack.LinearEnduranceCurve(1.)
380422
#curve.m = slope

pCrunch/test/test_aeroelastic_output.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import numpy.testing as npt
55
import pandas as pd
66
#import pandas.testing as pdt
7+
import copy
78

89
from pCrunch import AeroelasticOutput
910

@@ -133,6 +134,27 @@ def testConstructor(self):
133134
self.assertEqual(myobj.mc, mc)
134135
self.assertEqual(myobj.ec, [])
135136
self.assertEqual(myobj.fc, {})
137+
138+
def testNaNs(self):
139+
data2 = copy.deepcopy(data)
140+
data2["WindVxi"][-1] = np.nan
141+
myobj2 = AeroelasticOutput(data2, magnitude_channels=mc)
142+
self.assertEqual(myobj2.data.shape, (9,5))
143+
npt.assert_equal(myobj2.data[:,0], np.array(data2["Time"][:-1]))
144+
npt.assert_equal(myobj2.data[:,1], np.array(data2["WindVxi"][:-1]))
145+
npt.assert_equal(myobj2.data[:,2], np.zeros(9))
146+
npt.assert_equal(myobj2.data[:,3], np.zeros(9))
147+
npt.assert_equal(myobj2.data[:,4], np.array(data2["WindVxi"][:-1]))
148+
self.assertEqual(myobj2.channels, list(data.keys())+["Wind"])
149+
self.assertEqual(myobj2.units, None)
150+
self.assertEqual(myobj2.description, "")
151+
self.assertEqual(myobj2.filepath, "")
152+
self.assertEqual(myobj2.extreme_stat, "max")
153+
self.assertEqual(myobj2.td, ())
154+
self.assertEqual(myobj2.mc, mc)
155+
self.assertEqual(myobj2.ec, [])
156+
self.assertEqual(myobj2.fc, {})
157+
136158

137159
def testGetters(self):
138160
myobj = AeroelasticOutput(data, magnitude_channels=mc, dlc="/testdir/testfile")

0 commit comments

Comments
 (0)