From 876b336250a23f4d997006b18fad72610f43fed9 Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Sun, 5 Oct 2025 13:36:16 -0600 Subject: [PATCH 1/4] LineDesign Update: - Reorganizing the plotOptimization function to separate design variables from constraints and cost function. It also gives units to different variables and constraints. It is also color-referenced. - Adding units dictionary for constraints. - Adding two additional constraints related to the inclination angle of the line. --- famodel/design/LineDesign.py | 110 ++++++++++++++++++++++++++++------- 1 file changed, 88 insertions(+), 22 deletions(-) diff --git a/famodel/design/LineDesign.py b/famodel/design/LineDesign.py index 1ce0b695..9d40b34d 100644 --- a/famodel/design/LineDesign.py +++ b/famodel/design/LineDesign.py @@ -156,16 +156,22 @@ def __init__(self, depth, lineProps=None, **kwargs): Clump weights and buoyancy floats are not specified directly. They are both 'weights' and can have either a postitive or negative value ''' - # first set the weight, length, and diameter lists based on the allVars inputs. Don't worry about design variables yet + # 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. + if self.shared==1: if self.span == 0: raise Exception("For shared arrangements, a span must be provided to the Mooring object.") Ws = self.allVars[0::3].tolist() else: self.span = self.allVars[0]*10 - self.rBFair[0] # in tens of meters Ws = self.allVars[3::3].tolist() + Ls = self.allVars[1::3].tolist() Ds = self.allVars[2::3].tolist() - + + unitPattern = ['t', 'm', 'mm'] + self.allVarsUnits = [unitPattern[i % 3] for i in range(len(self.allVars))] + if self.shared==0: + self.allVarsUnits[0] = 'm' # if any of the input lengths are in ratio form, convert them to real value form # (this can currently only handle 1 ration variable per Mooring) if len(self.rInds) > 0: @@ -318,7 +324,7 @@ def __init__(self, depth, lineProps=None, **kwargs): # 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) self.X0 = np.array([self.allVars[self.Xindices.index(i)] for i in range(self.nX)]) - + self.X0Units = np.array(self.allVarsUnits)[[self.Xindices.index(i) for i in range(self.nX)]] # corresponding units self.X_denorm = np.ones(self.nX) # normalization factor for design variables self.obj_denorm = 1.0 # normalization factor for objective function @@ -340,10 +346,34 @@ def __init__(self, depth, lineProps=None, **kwargs): "max_sag" : self.con_max_sag, # a maximum for the lowest point's depth at x=x_extr_neg "max_total_length" : self.con_total_length, # a maximum line length "min_yaw_stiff" : self.con_yaw_stiffness, # a minimum yaw stiffness for the whole system about the extreme negative position - "max_damage" : self.con_damage, # a maximum fatigue damage for a specified mooring line (scales from provided damage from previous iteration) - "min_tension" : self.con_min_tension # a minimum line tension + "max_damage" : self.con_damage, # a maximum fatigue damage for a specified mooring line (scales from provided damage from previous iteration) + "min_tension" : self.con_min_tension, # a minimum line tension + "min_angle" : self.con_min_angle, # a minimum inclination angle for the line + "max_angle" : self.con_max_angle # a maximum inclination angle for the line } + # a hard-coded dictionary of the units associated with the constraints + conUnitsDict = {"min_Kx" : "N/m", + "max_offset" : "m", + "min_lay_length" : "m", + "rope_contact" : "m", + "tension_safety_factor" : "-", + "min_sag" : "m", + "max_sag" : "m", + "max_total_length" : "m", + "min_yaw_stiff" : "Nm/deg", + "max_damage" : "-", + "min_tension" : "N", + "min_angle" : "deg", + "max_angle" : "deg" + } + + # add units to the constraints dictionary + for con in self.constraints: + if con['name'] in conUnitsDict: + con['unit'] = conUnitsDict[con['name']] + else: + raise Exception(f"The constraint name '{con['name']}' is not recognized.") # set up list of active constraint functions self.conList = [] self.convals = np.zeros(len(self.constraints)) # array to hold constraint values @@ -1288,30 +1318,59 @@ def plotOptimization(self): if len(self.log['x']) == 0: print("No optimization trajectory saved (log is empty). Nothing to plot.") return - - fig, ax = plt.subplots(len(self.X0)+1+len(self.constraints),1, sharex=True, figsize=[6,8]) - fig.subplots_adjust(left=0.4) + Xs = np.array(self.log['x']) Fs = np.array(self.log['f']) Gs = np.array(self.log['g']) - for i in range(len(self.X0)): - ax[i].plot(Xs[:,i]) - #ax[i].axhline(self.Xmin[i], color=[0.5,0.5,0.5], dashes=[1,1]) - #ax[i].axhline(self.Xmax[i], color=[0.5,0.5,0.5], dashes=[1,1]) + n_dv = len(self.X0) + n_con = len(self.constraints) + n_rows = max(n_dv, n_con, 1) - ax[len(self.X0)].plot(Fs) - ax[len(self.X0)].set_ylabel("cost", rotation='horizontal') + fig, axes = plt.subplots(n_rows, 3, sharex=True, figsize=[12, 3*n_rows]) + fig.subplots_adjust(left=0.1, right=0.95, hspace=0.5, wspace=0.4) + + if n_rows == 1: + axes = axes[np.newaxis, :] - for i, con in enumerate(self.constraints): - j = i+1+len(self.X0) - ax[j].axhline(0, color=[0.5,0.5,0.5]) - ax[j].plot(Gs[:,i]) - ax[j].set_ylabel(f"{con['name']}({con['threshold']})", - rotation='horizontal', labelpad=80) + # --- Column 1: Design variables --- + for i in range(n_dv): + ax = axes[i, 0] + ax.plot(Xs[:, i], color='blue') + ax.set_title(f"d.v.{i+1} [{self.X0Units[i]}]") + + # turn off unused subplots + for i in range(n_dv, n_rows): + axes[i, 0].axis("off") - ax[j].set_xlabel("function evaluations") - + # --- Column 2: Constraints --- + for i, con in enumerate(self.constraints): + tol = 0.005 * (max(Gs[:, 0])-min(Gs[:, 0])) + ax = axes[i, 1] + ax.axhline(0, color=[0.5,0.5,0.5]) + color = 'green' if Gs[-1, i] >= -tol else 'red' + ax.plot(Gs[:, i], color=color) + ax.set_title(f"{con['name']} ({con['threshold']}) [{con['unit']}]") + + for i in range(n_con, n_rows): + axes[i, 1].axis("off") + + # --- Column 3: Cost --- + ax_cost = axes[0, 2] + ax_cost.plot(Fs/1e6, color='black') + ax_cost.set_title('cost [$M]') + + for i in range(1, n_rows): + axes[i, 2].axis("off") + + # x-label + for i in range(n_rows): + for j in range(3): + if axes[i, j].has_data(): + axes[i, j].set_xlabel("function evaluations") + + plt.tight_layout() + plt.savefig("/Users/ralkarem/Documents/projects/FADesign/scripts/concepts2/LineDesign_optimization_ex.pdf", dpi=200) def plotGA(self): '''A function dedicated to plotting relevant GA outputs''' @@ -1802,7 +1861,14 @@ def con_max_sag(self, X, index, threshold, display=0): a certain maximum depth.''' return self.ss.getSag(index) - threshold + # ----- angle constraints ----- + def con_min_angle(self, X, index, threshold, display=0): + '''Ensure the angle of a line section is above a minimum value.''' + return self.ss.getAng(index) - threshold + def con_max_angle(self, X, index, threshold, display=0): + '''Ensure the angle of a line section is below a maximum value.''' + return threshold - self.ss.getAng(index) # ----- utility functions ----- From 9a7d575d761cc2b49717f48e4cb1adfe49e36a7a Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Mon, 6 Oct 2025 13:03:56 -0600 Subject: [PATCH 2/4] improving on the plot Optimization function: - having option to switch between stacked 'tall' and a grid type. Default is tall. - having ylabels instead of titles. - other improvements to make sure there is no overlays between the subplots. --- famodel/design/LineDesign.py | 95 ++++++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/famodel/design/LineDesign.py b/famodel/design/LineDesign.py index 9d40b34d..2fb906fd 100644 --- a/famodel/design/LineDesign.py +++ b/famodel/design/LineDesign.py @@ -1313,8 +1313,14 @@ def getCons4PSO(self, X): return conList - def plotOptimization(self): - + def plotOptimization(self, layout="grid"): + '''Plot the optimization trajectory, including design variables, constraints and cost. + + Parameters + ---------- + layout : str + "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. + ''' if len(self.log['x']) == 0: print("No optimization trajectory saved (log is empty). Nothing to plot.") return @@ -1325,52 +1331,77 @@ def plotOptimization(self): n_dv = len(self.X0) n_con = len(self.constraints) - n_rows = max(n_dv, n_con, 1) - - fig, axes = plt.subplots(n_rows, 3, sharex=True, figsize=[12, 3*n_rows]) - fig.subplots_adjust(left=0.1, right=0.95, hspace=0.5, wspace=0.4) - if n_rows == 1: - axes = axes[np.newaxis, :] + if layout=="tall": + n_rows = n_dv + n_con + 1 + fig, axes = plt.subplots(n_rows, 1, sharex=True, figsize=[6, 1.5*n_rows]) + axes = axes.reshape(-1, 1) + elif layout=="grid": + n_rows = max(n_dv, n_con, 1) + fig, axes = plt.subplots(n_rows, 3, sharex=True, figsize=[12, 1.5*n_rows]) + if n_rows == 1: + axes = axes[np.newaxis, :] # --- Column 1: Design variables --- for i in range(n_dv): ax = axes[i, 0] ax.plot(Xs[:, i], color='blue') - ax.set_title(f"d.v.{i+1} [{self.X0Units[i]}]") - - # turn off unused subplots - for i in range(n_dv, n_rows): - axes[i, 0].axis("off") + ax.set_ylabel(f"d.v.{i+1} ({self.X0Units[i]})", rotation=0, labelpad=20,fontsize=10, ha='right', va='center') - # --- Column 2: Constraints --- + # --- Column 2 / stacked: Constraints --- for i, con in enumerate(self.constraints): - tol = 0.005 * (max(Gs[:, 0])-min(Gs[:, 0])) - ax = axes[i, 1] + idx = i if layout == "grid" else n_dv + i + ax = axes[idx, 1 if layout == "grid" else 0] ax.axhline(0, color=[0.5,0.5,0.5]) + tol = 0.005 * (max(Gs[:, i])-min(Gs[:, i])) color = 'green' if Gs[-1, i] >= -tol else 'red' ax.plot(Gs[:, i], color=color) - ax.set_title(f"{con['name']} ({con['threshold']}) [{con['unit']}]") - - for i in range(n_con, n_rows): - axes[i, 1].axis("off") - - # --- Column 3: Cost --- - ax_cost = axes[0, 2] + ax.set_ylabel(f"{con['name']} ({con['unit']})", + rotation=0, labelpad=20, + ha='right', va='center', + fontsize=10) + # Show threshold value inside plot + ax.text(0.98, 0.90, f"{con['threshold']}", + transform=ax.transAxes, + va='top', ha='right', fontsize=8, color='black') + + # --- Column 3 / stacked: Cost --- + if layout == "grid": + ax_cost = axes[0, 2] + else: + ax_cost = axes[-1, 0] ax_cost.plot(Fs/1e6, color='black') - ax_cost.set_title('cost [$M]') - - for i in range(1, n_rows): - axes[i, 2].axis("off") + ax_cost.set_ylabel('cost (M$)', rotation=0, labelpad=20, ha='right', va='center', fontsize=10) + + # remove unused axes if layout='grid' + if layout=="grid": + for i in range(n_dv, n_rows): + axes[i, 0].axis('off') + + for i in range(n_con, n_rows): + axes[i, 1].axis('off') + + for i in range(1, n_rows): + axes[i, 2].axis('off') - # x-label + # --- X labels only on bottom subplots --- for i in range(n_rows): - for j in range(3): + for j in range(axes.shape[1]): if axes[i, j].has_data(): - axes[i, j].set_xlabel("function evaluations") - + if layout == "tall": + if i == n_rows-1: # only bottom row + axes[i, j].set_xlabel("function evaluations") + else: + axes[i, j].set_xlabel("") + elif layout == "grid": + if (i == n_dv-1 and j == 0) or (i == n_con-1 and j == 1) or (i == 0 and j == 2): + axes[i, j].set_xlabel("function evaluations") + else: + axes[i, j].set_xlabel("") + + plt.tight_layout() - plt.savefig("/Users/ralkarem/Documents/projects/FADesign/scripts/concepts2/LineDesign_optimization_ex.pdf", dpi=200) + def plotGA(self): '''A function dedicated to plotting relevant GA outputs''' From 447b3c0f68a68ab09d0c302ca95ce51e16a9744b Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Mon, 6 Oct 2025 14:02:13 -0600 Subject: [PATCH 3/4] switching default layout to tall --- famodel/design/LineDesign.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/famodel/design/LineDesign.py b/famodel/design/LineDesign.py index 2fb906fd..ca6908a0 100644 --- a/famodel/design/LineDesign.py +++ b/famodel/design/LineDesign.py @@ -1313,7 +1313,7 @@ def getCons4PSO(self, X): return conList - def plotOptimization(self, layout="grid"): + def plotOptimization(self, layout="tall"): '''Plot the optimization trajectory, including design variables, constraints and cost. Parameters From c4b24f48c1841ae3d78e2f4342ae73cce7b8f56e Mon Sep 17 00:00:00 2001 From: Yuksel-Rudy Date: Thu, 9 Oct 2025 11:42:34 -0600 Subject: [PATCH 4/4] adding feature to retreive figure handles when calling plotOptimization function in LineDesign. --- famodel/design/LineDesign.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/famodel/design/LineDesign.py b/famodel/design/LineDesign.py index ca6908a0..f9facb16 100644 --- a/famodel/design/LineDesign.py +++ b/famodel/design/LineDesign.py @@ -1302,7 +1302,7 @@ def negativeObjectiveFun(**kwargs): if plot: self.plotOptimization() - + return X, self.cost #, infodict @@ -1313,7 +1313,7 @@ def getCons4PSO(self, X): return conList - def plotOptimization(self, layout="tall"): + def plotOptimization(self, layout="tall", return_fig=False): '''Plot the optimization trajectory, including design variables, constraints and cost. Parameters @@ -1346,7 +1346,7 @@ def plotOptimization(self, layout="tall"): for i in range(n_dv): ax = axes[i, 0] ax.plot(Xs[:, i], color='blue') - ax.set_ylabel(f"d.v.{i+1} ({self.X0Units[i]})", rotation=0, labelpad=20,fontsize=10, ha='right', va='center') + ax.set_ylabel(f"d.v.{i+1} ({self.X0Units[i]})", rotation=90, fontsize=10, va='center') # --- Column 2 / stacked: Constraints --- for i, con in enumerate(self.constraints): @@ -1356,10 +1356,13 @@ def plotOptimization(self, layout="tall"): tol = 0.005 * (max(Gs[:, i])-min(Gs[:, i])) color = 'green' if Gs[-1, i] >= -tol else 'red' ax.plot(Gs[:, i], color=color) - ax.set_ylabel(f"{con['name']} ({con['unit']})", - rotation=0, labelpad=20, - ha='right', va='center', - fontsize=10) + if con['name']=='tension_safety_factor': + con_name = 'SF' + else: + con_name = con['name'] + ax.set_ylabel(f"{con_name} ({con['unit']})", + rotation=90, + va='center', fontsize=10) # Show threshold value inside plot ax.text(0.98, 0.90, f"{con['threshold']}", transform=ax.transAxes, @@ -1371,8 +1374,8 @@ def plotOptimization(self, layout="tall"): else: ax_cost = axes[-1, 0] ax_cost.plot(Fs/1e6, color='black') - ax_cost.set_ylabel('cost (M$)', rotation=0, labelpad=20, ha='right', va='center', fontsize=10) - + ax_cost.set_ylabel('cost (M$)', rotation=90, va='center', fontsize=10) + # remove unused axes if layout='grid' if layout=="grid": for i in range(n_dv, n_rows): @@ -1381,7 +1384,7 @@ def plotOptimization(self, layout="tall"): for i in range(n_con, n_rows): axes[i, 1].axis('off') - for i in range(1, n_rows): + for i in range(2, n_rows): axes[i, 2].axis('off') # --- X labels only on bottom subplots --- @@ -1402,6 +1405,9 @@ def plotOptimization(self, layout="tall"): plt.tight_layout() + if return_fig: + return fig, axes + def plotGA(self): '''A function dedicated to plotting relevant GA outputs''' @@ -2100,7 +2106,7 @@ def dump(self): info['design']['X' ] = self.Xlast.tolist() # design variables #info['design']['Gdict' ] = self.evaluateConstraints([])[1] # dict of constraint names and values of evaluated constraint functions info['design']['Ls' ] = [float(line.L ) for line in self.ss.lineList] # length of each segment - info['design']['Ds' ] = [float(line.type['input_d']) for line in self.ss.lineList] # *input* diameter of each segment + info['design']['Ds' ] = [float(line.type['d_nom']) for line in self.ss.lineList] # *input* diameter of each segment 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) info['design']['anchorType'] = self.anchorType # (may be redundant with what's in arrangement) info['design']['span' ] = float(self.span) # platform-platform of platfom-anchor horizontal span just in case it's changed