Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/CI_WEIS.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@ jobs:
- name: Test model creation notebooks and drivers
if: contains( matrix.os, 'ubuntu') && contains( github.event_name, 'pull_request')
run: |
cd examples/11_model_creation_process
treon 0_notebooks/chapter1.ipynb
treon 0_notebooks/chapter2.ipynb
treon 0_notebooks/chapter3.ipynb
cd examples/11_model_creation_process/0_notebooks
treon chapter1.ipynb
treon chapter2.ipynb
treon chapter3.ipynb

# Run parallel script calling OpenFAST
- name: Run parallel cases
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ design_variables:
lower_bound: -30.0
upper_bound: -12.0
flag: True
# - names: [col1_freeboard, col2_freeboard, col3_freeboard]
# lower_bound: 10.0
# upper_bound: 20.0
r_coordinate:
- names: [col1_keel, col1_freeboard, col2_keel, col2_freeboard, col3_keel, col3_freeboard]
lower_bound: 50.0
Expand All @@ -35,18 +32,7 @@ design_variables:
lower_bound: 12.0
upper_bound: 16.0
constant: True
# ballast:
# lower_bound: 0
# upper_bound: 1000
# - names: [main_column]
# ballast:
# lower_bound: 0
# upper_bound: 1000
# - names: [Y_pontoon_lower1, Y_pontoon_lower2, Y_pontoon_lower3]
# diameter:
# lower_bound: 10.0
# upper_bound: 10.8
# constant: True

constraints:
tower:
height_constraint:
Expand Down Expand Up @@ -91,14 +77,6 @@ constraints:
flag: True
buoyancy:
flag: False
stress:
flag: False
global_buckling:
flag: False
shell_buckling:
flag: False
mooring_heel:
flag: False
freeboard_margin: # keep freeboard from being submerged below water during survival_heel, largest wave
flag: True
draft_margin: # keep draft from raising above water line during survival_heel, largest wave
Expand All @@ -109,9 +87,6 @@ constraints:
Max_PtfmPitch:
flag: True
max: 6.0
Std_PtfmPitch:
flag: False
max: 1.25 # Same as IEA-15MW with same DLCs
nacelle_acceleration:
flag: True
max: 2.0
Expand Down
3 changes: 3 additions & 0 deletions weis/aeroelasticse/openmdao_openfast.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ def setup(self):

# Floating outputs
self.add_output('Max_PtfmPitch', val=0.0, desc='Maximum platform pitch angle over a set of OpenFAST simulations')
self.add_output('Mean_PtfmPitch', val=0.0, units='deg', desc='Maximum (across cases) mean (of each case) platform pitch angle over a set of OpenFAST simulations')
self.add_output('Std_PtfmPitch', val=0.0, units='deg', desc='standard deviation of platform pitch angle')
self.add_output('Max_Offset', val=0.0, units='m', desc='Maximum distance in surge/sway direction')

Expand Down Expand Up @@ -3459,6 +3460,7 @@ def get_floating_measures(self, inputs, outputs):
calculate floating measures:
- Std_PtfmPitch (max over all dlcs if constraint, mean otheriwse)
- Max_PtfmPitch
- Mean_PtfmPitch

given:
- sum_stats : pd.DataFrame
Expand All @@ -3472,6 +3474,7 @@ def get_floating_measures(self, inputs, outputs):
outputs['Std_PtfmPitch'] = np.mean(sum_stats['PtfmPitch']['std'])

outputs['Max_PtfmPitch'] = np.max(sum_stats['PtfmPitch']['max'])
outputs['Mean_PtfmPitch'] = np.max(sum_stats['PtfmPitch']['mean'])

# Max platform offset
outputs['Max_Offset'] = np.max(sum_stats['PtfmOffset']['max'])
Expand Down
9 changes: 8 additions & 1 deletion weis/glue_code/gc_PoseOptimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,14 @@ def set_constraints(self, wt_opt):
raise Exception('Please turn on the call to OpenFAST or RAFT if you are trying to optimize Max_PtfmPitch constraints.')
wt_opt.model.add_constraint(f'{self.floating_solve_component}.Max_PtfmPitch',
upper = control_constraints['Max_PtfmPitch']['max'])


# Mean platform pitch
if control_constraints['Mean_PtfmPitch']['flag']:
if not any(self.level_flags):
raise Exception('Please turn on the call to OpenFAST or RAFT if you are trying to optimize Mean_PtfmPitch constraints.')
wt_opt.model.add_constraint(f'{self.floating_solve_component}.Mean_PtfmPitch',
upper = control_constraints['Mean_PtfmPitch']['max'])

# Platform pitch motion
if control_constraints['Std_PtfmPitch']['flag']:
if not any(self.level_flags):
Expand Down
14 changes: 13 additions & 1 deletion weis/inputs/analysis_schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,21 @@ properties:
minimum: 0.0
maximum: 30.0
unit: deg
Mean_PtfmPitch:
type: object
description: The maximum mean platform pitch displacement over all cases. Can be computed in both RAFT and OpenFAST. The higher fidelity option will be used when active.
default: {}
properties:
flag: *flag
max:
type: number
default: 3.0
minimum: 0.0
maximum: 30.0
unit: deg
Std_PtfmPitch:
type: object
description: Maximum platform pitch standard deviation over all cases. Can be computed in both RAFT and OpenFAST. The higher fidelity option will be used when active.
description: Maximum platform pitch standard deviation over all cases. Can be computed in both RAFT and OpenFAST. The higher fidelity option will be used when active.
default: {}
properties:
flag: *flag
Expand Down
38 changes: 37 additions & 1 deletion weis/inputs/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,40 @@ def write_analysis_yaml(instance, foutput):
sfx_str = "-analysis.yaml"
wisval.write_yaml(instance, foutput + sfx_str)
return foutput + sfx_str



def make_paths_absolute(data, base_dir=None):
"""Recursively convert relative paths in a nested dict/list to absolute paths.

Any string value that contains ``/`` or ``\\`` and is not already an
absolute path is joined with *base_dir* and resolved to an absolute path.
This is useful when loading YAML configuration files that contain
relative references to other files or directories.

Parameters
----------
data : dict, list, or str
The data structure to process (typically the result of
``yaml.safe_load``).
base_dir : str, optional
The directory that relative paths are relative to. Defaults to
the current working directory.

Returns
-------
data : dict, list, or str
A copy of the input with relative paths replaced by absolute paths.
"""
if base_dir is None:
base_dir = os.getcwd()

if isinstance(data, dict):
return {k: make_paths_absolute(v, base_dir) for k, v in data.items()}
elif isinstance(data, list):
return [make_paths_absolute(item, base_dir) for item in data]
elif isinstance(data, str):
if ("/" in data or "\\" in data) and not os.path.isabs(data):
return os.path.abspath(os.path.join(base_dir, data))
return data
else:
return data
83 changes: 83 additions & 0 deletions weis/visualization/opt_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,86 @@ def plot_conv(

return fig, axes


def plot_convergence(data, vars_to_plot, title_prefix, bounds=None,
aliases=None, save_path=None):
"""Plot iteration history for a list of recorded variables with optional bound lines.

This is a lightweight alternative to :func:`plot_conv` that works directly
with the dict returned by :func:`~weis.visualization.utils.load_OMsql`
and accepts a *bounds* dict (e.g. from
:func:`~weis.visualization.utils.load_problem_vars_yaml` or
:func:`~weis.visualization.utils.load_bounds_from_analysis_yaml`).

Parameters
----------
data : dict
``{var_name: list_of_values_per_iteration}`` as returned by
:func:`~weis.visualization.utils.load_OMsql`.
vars_to_plot : list[str]
OpenMDAO variable names to include in the figure.
title_prefix : str
Text used as the figure super-title.
bounds : dict, optional
``{var_name: {"lower": float | None, "upper": float | None}}``.
When provided, horizontal dashed lines are drawn at the bound values.
aliases : dict, optional
``{var_name: "Human-Readable Label"}``. Falls back to the raw
variable name when not provided.
save_path : str, optional
If given, the figure is saved to this path (PNG recommended).

Returns
-------
fig : matplotlib.figure.Figure or None
The generated figure, or ``None`` if no plottable variables were found.
"""
bounds = bounds or {}
aliases = aliases or {}

# Filter to variables actually present in data
vars_present = [v for v in vars_to_plot if v in data]
vars_missing = [v for v in vars_to_plot if v not in data]
if vars_missing:
print(f" Skipping (not recorded): {vars_missing}")
if not vars_present:
print(f" No variables found for '{title_prefix}' — skipping plot.")
return None

n = len(vars_present)
fig, axes = plt.subplots(n, 1, figsize=(10, 3 * n), sharex=True)
if n == 1:
axes = [axes]
fig.suptitle(title_prefix, fontsize=13)

for ax, var in zip(axes, vars_present):
vals = np.array(data[var])
if vals.ndim == 1:
ax.plot(vals, marker="o", ms=4)
else:
for i in range(vals.shape[1]):
ax.plot(vals[:, i], marker="o", ms=4, label=f"[{i}]")
ax.legend(fontsize=8)

# Draw bounds as horizontal dashed lines
if var in bounds:
if bounds[var].get("upper") is not None:
ax.axhline(bounds[var]["upper"], color="r", ls="--", lw=1.2,
label=f"upper={bounds[var]['upper']:.3g}")
if bounds[var].get("lower") is not None:
ax.axhline(bounds[var]["lower"], color="b", ls="--", lw=1.2,
label=f"lower={bounds[var]['lower']:.3g}")
ax.legend(fontsize=8)

ax.set_title(aliases.get(var, var), pad=3)
ax.set_ylabel("value", labelpad=4)
ax.grid(True)

axes[-1].set_xlabel("Optimizer iteration")
plt.tight_layout(rect=[0, 0, 1, 0.98])
if save_path:
fig.savefig(save_path, dpi=150, bbox_inches="tight")
print(f" Saved: {save_path}")
plt.close(fig)
return fig

126 changes: 126 additions & 0 deletions weis/visualization/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,132 @@ def load_vars_file(fn_vars):
return vars


def load_problem_vars_yaml(fn_vars):
"""
Load upper/lower bounds from a problem_vars.yaml file written by WEIS.

Parameters
----------
fn_vars : str
Path to the problem_vars.yaml file.

Returns
-------
bounds : dict
``{var_name: {"lower": float | None, "upper": float | None}}``
Values whose magnitude exceeds 1e28 are treated as unconstrained
(returned as ``None``).
"""
INF = 1e28
bounds = {}
if not os.path.isfile(fn_vars):
return bounds
with open(fn_vars) as f:
pv = yaml.safe_load(f)
for section in ("design_vars", "constraints"):
for entry in pv.get(section, []):
meta = entry[1] # each entry is [name_str, meta_dict]
name = meta["name"]
lo = meta.get("lower", None)
hi = meta.get("upper", None)
bounds[name] = {
"lower": float(lo) if lo not in (None, "", "''") and abs(float(lo)) < INF else None,
"upper": float(hi) if hi not in (None, "", "''") and abs(float(hi)) < INF else None,
}
return bounds


def load_bounds_from_analysis_yaml(analysis_yaml_path):
"""
Build a bounds dict from a WEIS/WISDEM analysis_options.yaml file.

Maps common tower design variable and constraint entries to their
OpenMDAO variable names so that bound lines can be drawn on
convergence plots.

Parameters
----------
analysis_yaml_path : str
Path to the analysis_options.yaml file.

Returns
-------
bounds : dict
``{om_var_name: {"lower": float | None, "upper": float | None}}``
"""
bounds = {}
if not os.path.isfile(analysis_yaml_path):
return bounds
with open(analysis_yaml_path) as f:
opts = yaml.safe_load(f)

# --- Design variable bounds ---
dv = opts.get("design_variables", {}).get("tower", {})
if dv.get("layer_thickness", {}).get("flag"):
bounds["tower.layer_thickness"] = {
"lower": dv["layer_thickness"].get("lower_bound"),
"upper": dv["layer_thickness"].get("upper_bound"),
}
if dv.get("outer_diameter", {}).get("flag"):
bounds["tower.diameter"] = {
"lower": dv["outer_diameter"].get("lower_bound"),
"upper": dv["outer_diameter"].get("upper_bound"),
}

# --- Constraint bounds ---
tc = opts.get("constraints", {}).get("tower", {})

# Normalized constraints (upper = 1.0)
for yaml_key, om_name in [
("stress", "towerse.post.constr_stress"),
("global_buckling", "towerse.post.constr_global_buckling"),
("shell_buckling", "towerse.post.constr_shell_buckling"),
("slope", "towerse.slope"),
("thickness_slope", "towerse.thickness_slope"),
]:
if tc.get(yaml_key, {}).get("flag"):
bounds[om_name] = {"lower": None, "upper": 1.0}

# d_to_t has explicit lower/upper
if tc.get("d_to_t", {}).get("flag"):
bounds["towerse.constr_d_to_t"] = {
"lower": tc["d_to_t"].get("lower_bound"),
"upper": tc["d_to_t"].get("upper_bound"),
}

# taper has explicit lower
if tc.get("taper", {}).get("flag"):
bounds["towerse.constr_taper"] = {
"lower": tc["taper"].get("lower_bound"),
"upper": None,
}

# frequency_1 has lower/upper
if tc.get("frequency_1", {}).get("flag"):
bounds["floatingse.structural_frequencies"] = {
"lower": tc["frequency_1"].get("lower_bound"),
"upper": tc["frequency_1"].get("upper_bound"),
}

# --- Floating platform constraints ---
fc = opts.get("constraints", {}).get("floating", {})
for yaml_key, om_name in [
("Max_PtfmPitch", "raft.Max_PtfmPitch"),
("Mean_PtfmPitch", "raft.Mean_PtfmPitch"),
("max_nac_accel", "raft.max_nac_accel"),
("heave_period", "raft.heave_period"),
("pitch_period", "raft.pitch_period"),
]:
entry = fc.get(yaml_key, {})
if entry.get("flag"):
bounds[om_name] = {
"lower": entry.get("lower_bound"),
"upper": entry.get("upper_bound"),
}

return bounds


def compare_om_data(
dataOM_1,
dataOM_2,
Expand Down
Loading