Skip to content

Commit 956ed5e

Browse files
authored
Merge pull request #27 from FloatingArrayDesign/LineDesignUpdate
Reorganizing the plotOptimization function to This pull request enhances the LineDesign class by adding unit tracking for design variables and constraints, introducing new angle constraints, and improving the plotting of optimization results. The changes improve clarity, traceability, and usability for both developers and users working with mooring line design optimization. Units tracking and assignment Added allVarsUnits to track units for each design variable, and X0Units to associate units with the active design variables. This ensures that units are consistently available throughout the optimization process. [1] [2] Implemented a dictionary (conUnitsDict) to assign units to each constraint, and updated the constraints dictionary to include these units. An exception is raised if a constraint name is not recognized. New constraints Added two new constraint functions: con_min_angle and con_max_angle, enforcing minimum and maximum inclination angles for line sections. These are now available for use in optimization. [1] [2] Plotting improvements Refactored and enhanced the plotOptimization method to support both "tall" and "grid" layouts, display units for design variables and constraints, and allow returning the figure and axes for further customization.
2 parents 9ad867f + c4b24f4 commit 956ed5e

File tree

1 file changed

+130
-27
lines changed

1 file changed

+130
-27
lines changed

famodel/design/LineDesign.py

Lines changed: 130 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -156,16 +156,22 @@ def __init__(self, depth, lineProps=None, **kwargs):
156156
Clump weights and buoyancy floats are not specified directly. They are both 'weights' and can have either a postitive or negative value
157157
'''
158158

159-
# first set the weight, length, and diameter lists based on the allVars inputs. Don't worry about design variables yet
159+
# first set the weight, length, and diameter lists based on the allVars inputs. Don't worry about design variables yet. Create the units list too.
160+
160161
if self.shared==1:
161162
if self.span == 0: raise Exception("For shared arrangements, a span must be provided to the Mooring object.")
162163
Ws = self.allVars[0::3].tolist()
163164
else:
164165
self.span = self.allVars[0]*10 - self.rBFair[0] # in tens of meters
165166
Ws = self.allVars[3::3].tolist()
167+
166168
Ls = self.allVars[1::3].tolist()
167169
Ds = self.allVars[2::3].tolist()
168-
170+
171+
unitPattern = ['t', 'm', 'mm']
172+
self.allVarsUnits = [unitPattern[i % 3] for i in range(len(self.allVars))]
173+
if self.shared==0:
174+
self.allVarsUnits[0] = 'm'
169175
# if any of the input lengths are in ratio form, convert them to real value form
170176
# (this can currently only handle 1 ration variable per Mooring)
171177
if len(self.rInds) > 0:
@@ -318,7 +324,7 @@ def __init__(self, depth, lineProps=None, **kwargs):
318324

319325
# fill in the X0 value (initial design variable values) based on provided allVars and Xindices (uses first value if a DV has multiple in allVars)
320326
self.X0 = np.array([self.allVars[self.Xindices.index(i)] for i in range(self.nX)])
321-
327+
self.X0Units = np.array(self.allVarsUnits)[[self.Xindices.index(i) for i in range(self.nX)]] # corresponding units
322328
self.X_denorm = np.ones(self.nX) # normalization factor for design variables
323329
self.obj_denorm = 1.0 # normalization factor for objective function
324330

@@ -340,10 +346,34 @@ def __init__(self, depth, lineProps=None, **kwargs):
340346
"max_sag" : self.con_max_sag, # a maximum for the lowest point's depth at x=x_extr_neg
341347
"max_total_length" : self.con_total_length, # a maximum line length
342348
"min_yaw_stiff" : self.con_yaw_stiffness, # a minimum yaw stiffness for the whole system about the extreme negative position
343-
"max_damage" : self.con_damage, # a maximum fatigue damage for a specified mooring line (scales from provided damage from previous iteration)
344-
"min_tension" : self.con_min_tension # a minimum line tension
349+
"max_damage" : self.con_damage, # a maximum fatigue damage for a specified mooring line (scales from provided damage from previous iteration)
350+
"min_tension" : self.con_min_tension, # a minimum line tension
351+
"min_angle" : self.con_min_angle, # a minimum inclination angle for the line
352+
"max_angle" : self.con_max_angle # a maximum inclination angle for the line
345353
}
346354

355+
# a hard-coded dictionary of the units associated with the constraints
356+
conUnitsDict = {"min_Kx" : "N/m",
357+
"max_offset" : "m",
358+
"min_lay_length" : "m",
359+
"rope_contact" : "m",
360+
"tension_safety_factor" : "-",
361+
"min_sag" : "m",
362+
"max_sag" : "m",
363+
"max_total_length" : "m",
364+
"min_yaw_stiff" : "Nm/deg",
365+
"max_damage" : "-",
366+
"min_tension" : "N",
367+
"min_angle" : "deg",
368+
"max_angle" : "deg"
369+
}
370+
371+
# add units to the constraints dictionary
372+
for con in self.constraints:
373+
if con['name'] in conUnitsDict:
374+
con['unit'] = conUnitsDict[con['name']]
375+
else:
376+
raise Exception(f"The constraint name '{con['name']}' is not recognized.")
347377
# set up list of active constraint functions
348378
self.conList = []
349379
self.convals = np.zeros(len(self.constraints)) # array to hold constraint values
@@ -1272,7 +1302,7 @@ def negativeObjectiveFun(**kwargs):
12721302

12731303
if plot:
12741304
self.plotOptimization()
1275-
1305+
12761306
return X, self.cost #, infodict
12771307

12781308

@@ -1283,35 +1313,101 @@ def getCons4PSO(self, X):
12831313
return conList
12841314

12851315

1286-
def plotOptimization(self):
1287-
1316+
def plotOptimization(self, layout="tall", return_fig=False):
1317+
'''Plot the optimization trajectory, including design variables, constraints and cost.
1318+
1319+
Parameters
1320+
----------
1321+
layout : str
1322+
"tall" (default) or "grid" layout for subplots. The grid will place all d.v.s in the first column, all constraints in the second column, and cost in the third column.
1323+
'''
12881324
if len(self.log['x']) == 0:
12891325
print("No optimization trajectory saved (log is empty). Nothing to plot.")
12901326
return
1291-
1292-
fig, ax = plt.subplots(len(self.X0)+1+len(self.constraints),1, sharex=True, figsize=[6,8])
1293-
fig.subplots_adjust(left=0.4)
1327+
12941328
Xs = np.array(self.log['x'])
12951329
Fs = np.array(self.log['f'])
12961330
Gs = np.array(self.log['g'])
12971331

1298-
for i in range(len(self.X0)):
1299-
ax[i].plot(Xs[:,i])
1300-
#ax[i].axhline(self.Xmin[i], color=[0.5,0.5,0.5], dashes=[1,1])
1301-
#ax[i].axhline(self.Xmax[i], color=[0.5,0.5,0.5], dashes=[1,1])
1302-
1303-
ax[len(self.X0)].plot(Fs)
1304-
ax[len(self.X0)].set_ylabel("cost", rotation='horizontal')
1305-
1332+
n_dv = len(self.X0)
1333+
n_con = len(self.constraints)
1334+
1335+
if layout=="tall":
1336+
n_rows = n_dv + n_con + 1
1337+
fig, axes = plt.subplots(n_rows, 1, sharex=True, figsize=[6, 1.5*n_rows])
1338+
axes = axes.reshape(-1, 1)
1339+
elif layout=="grid":
1340+
n_rows = max(n_dv, n_con, 1)
1341+
fig, axes = plt.subplots(n_rows, 3, sharex=True, figsize=[12, 1.5*n_rows])
1342+
if n_rows == 1:
1343+
axes = axes[np.newaxis, :]
1344+
1345+
# --- Column 1: Design variables ---
1346+
for i in range(n_dv):
1347+
ax = axes[i, 0]
1348+
ax.plot(Xs[:, i], color='blue')
1349+
ax.set_ylabel(f"d.v.{i+1} ({self.X0Units[i]})", rotation=90, fontsize=10, va='center')
1350+
1351+
# --- Column 2 / stacked: Constraints ---
13061352
for i, con in enumerate(self.constraints):
1307-
j = i+1+len(self.X0)
1308-
ax[j].axhline(0, color=[0.5,0.5,0.5])
1309-
ax[j].plot(Gs[:,i])
1310-
ax[j].set_ylabel(f"{con['name']}({con['threshold']})",
1311-
rotation='horizontal', labelpad=80)
1353+
idx = i if layout == "grid" else n_dv + i
1354+
ax = axes[idx, 1 if layout == "grid" else 0]
1355+
ax.axhline(0, color=[0.5,0.5,0.5])
1356+
tol = 0.005 * (max(Gs[:, i])-min(Gs[:, i]))
1357+
color = 'green' if Gs[-1, i] >= -tol else 'red'
1358+
ax.plot(Gs[:, i], color=color)
1359+
if con['name']=='tension_safety_factor':
1360+
con_name = 'SF'
1361+
else:
1362+
con_name = con['name']
1363+
ax.set_ylabel(f"{con_name} ({con['unit']})",
1364+
rotation=90,
1365+
va='center', fontsize=10)
1366+
# Show threshold value inside plot
1367+
ax.text(0.98, 0.90, f"{con['threshold']}",
1368+
transform=ax.transAxes,
1369+
va='top', ha='right', fontsize=8, color='black')
1370+
1371+
# --- Column 3 / stacked: Cost ---
1372+
if layout == "grid":
1373+
ax_cost = axes[0, 2]
1374+
else:
1375+
ax_cost = axes[-1, 0]
1376+
ax_cost.plot(Fs/1e6, color='black')
1377+
ax_cost.set_ylabel('cost (M$)', rotation=90, va='center', fontsize=10)
1378+
1379+
# remove unused axes if layout='grid'
1380+
if layout=="grid":
1381+
for i in range(n_dv, n_rows):
1382+
axes[i, 0].axis('off')
1383+
1384+
for i in range(n_con, n_rows):
1385+
axes[i, 1].axis('off')
1386+
1387+
for i in range(2, n_rows):
1388+
axes[i, 2].axis('off')
1389+
1390+
# --- X labels only on bottom subplots ---
1391+
for i in range(n_rows):
1392+
for j in range(axes.shape[1]):
1393+
if axes[i, j].has_data():
1394+
if layout == "tall":
1395+
if i == n_rows-1: # only bottom row
1396+
axes[i, j].set_xlabel("function evaluations")
1397+
else:
1398+
axes[i, j].set_xlabel("")
1399+
elif layout == "grid":
1400+
if (i == n_dv-1 and j == 0) or (i == n_con-1 and j == 1) or (i == 0 and j == 2):
1401+
axes[i, j].set_xlabel("function evaluations")
1402+
else:
1403+
axes[i, j].set_xlabel("")
1404+
1405+
1406+
plt.tight_layout()
1407+
1408+
if return_fig:
1409+
return fig, axes
13121410

1313-
ax[j].set_xlabel("function evaluations")
1314-
13151411
def plotGA(self):
13161412
'''A function dedicated to plotting relevant GA outputs'''
13171413

@@ -1802,7 +1898,14 @@ def con_max_sag(self, X, index, threshold, display=0):
18021898
a certain maximum depth.'''
18031899
return self.ss.getSag(index) - threshold
18041900

1901+
# ----- angle constraints -----
1902+
def con_min_angle(self, X, index, threshold, display=0):
1903+
'''Ensure the angle of a line section is above a minimum value.'''
1904+
return self.ss.getAng(index) - threshold
18051905

1906+
def con_max_angle(self, X, index, threshold, display=0):
1907+
'''Ensure the angle of a line section is below a maximum value.'''
1908+
return threshold - self.ss.getAng(index)
18061909

18071910
# ----- utility functions -----
18081911

@@ -2003,7 +2106,7 @@ def dump(self):
20032106
info['design']['X' ] = self.Xlast.tolist() # design variables
20042107
#info['design']['Gdict' ] = self.evaluateConstraints([])[1] # dict of constraint names and values of evaluated constraint functions
20052108
info['design']['Ls' ] = [float(line.L ) for line in self.ss.lineList] # length of each segment
2006-
info['design']['Ds' ] = [float(line.type['input_d']) for line in self.ss.lineList] # *input* diameter of each segment
2109+
info['design']['Ds' ] = [float(line.type['d_nom']) for line in self.ss.lineList] # *input* diameter of each segment
20072110
info['design']['lineTypes' ] = [str(line.type['material']) for line in self.ss.lineList] # line type of each segment (may be redundant with what's in arrangement)
20082111
info['design']['anchorType'] = self.anchorType # (may be redundant with what's in arrangement)
20092112
info['design']['span' ] = float(self.span) # platform-platform of platfom-anchor horizontal span just in case it's changed

0 commit comments

Comments
 (0)