import numpy as np
import matplotlib.pyplot as plt
from tabulate import tabulate
from .utils.exceptions import InputError, RockGradingError, user_warning, NotSupportedError
from .core.stability import vandermeer, hudson
from .core.toe import toe_stability
from .core.overtopping import rubble_mound
from .core.scour import scour_protection
from .core.bishop import Bishop
from .core import substructure
class RubbleMound:
""" General Rubble Mound breakwater class
Makes a conceptual design for the substructure of a rubble mound
breakwater. The class computes the necessary nominal diameter and
rock class of the underlayer, and an filter layer if one is needed.
Depending on the rock class it is possible that a new variant is
generated, these are identified by an a, b, c or d. In the attribute
:py:attr:`variantIDs` a list of generated variants is stored.
Furthermore, it computes the required crest freeboard of the
breakwater by using the maximum allowed overtopping discharge from
the LimitStates with the formulas from EurOtop (2018). Additionally,
the toe of the breakwater is designed with toe stability formula of
Van der Meer (1998).
Parameters
----------
Dn50 : float
Dn50 of the armour [m]
Dn50_core : float
nominal diameter for the stones in the core of the breakwater [m]
rho : float
density of the material of the armour layer [kg/m³]
rho_w : float
density of water [kg/m³]
armour_layer : str
type of armour layer, fully supported armour layers are Rock,
Xbloc and XblocPlus. Full support means that the rules for the
underlayer have only been included for these types of armour
layer. In case another armour layer is used the rule for the
underlayer, the :py:obj:`filter_rule` must manually be set.
layers : int
number of layers in the armour layer
LimitStates : list
list of :py:class:`LimitState` ULS, SLS or other defined limit
states defined with :py:class:`LimitState`. When designing with
one limit state it must still be entered as a list.
Grading : :py:class:`RockGrading`
standard rock grading defined in the NEN-EN 13383-1 or a user
defined rock grading
safety : float, optional, default: 1
safety factor of design (number of standard deviations from the
mean). Positive values increase the safety, negative values
decrease the safety of the breakwater
layers_underlayer : int, optional, default: 2
number of layers in the underlayer
slope_toe : tuple, optional, default: (2, 3)
slope of the toe
B_toe : float, optional, default: None
width of the top of the toe in meters. By default the width of
toe is taken as 3 * Dn50_toe.
slope : tuple, optional, default: None
Slope of the armour layer (V, H). For example a slope of 3V:4H
is defined as (3, 4)
B : float, optional, default: None
Crest width [m]
beta : float, optional, default: 0
angle between direction of wave approach and a line normal to
the breakwater (degrees).
filter_rule : {'Rock', 'Xbloc', 'XblocPlus'}, optional, default: None
filter rule to use for the substructure of the breakwater, for
Rock, Xbloc and XblocPlus the correct filter rule is
automatically selected. In case another type of armour layer is
used one of these filter rules must be chosen.
Soil : :py:class:`Soil`, optional, default: None
by default Soil is None, which means that the geotechnical checks
are not performed. By specifying a Soil object, the geotechnical
checks are automatically performed.
phi : float, optional, default: 40
internal friction angle of rock [degrees]
id : int, optional, default: None
add a unique id to the breakwater
Attributes
----------
logger : dict
dict of warnings and messages
structure : dict
dictionary with the computed Dn50, rock class, and average Dn50
of the rock class for each layer and the toe. This dictionary
includes all variants, use :py:meth:`get_variant` to get the
parameters of one specific variant. Alternatively,
:py:meth:`print_variants` can be used to print the details of
one, multiple or all variants.
alpha : float
slope of the structure in radians
id : int
unique id of the breakwater
variantIDs : list
list with the IDs of the variants generated for this rubble
mound breakwater.
Rc : float
the crest freeboard of the structure [m]
width_scour : float
the required length of the scour protection [m]
bishop : :py:class:`Bishop`
the :py:class:`Bishop` that was normative in the computation
F_norm : float
normative factor of safety computed with :py:class:`Bishop`
"""
def __init__(
self, Dn50, Dn50_core, rho, rho_w, armour_layer, layers,
LimitStates, Grading, safety=1, layers_underlayer=2,
slope_toe=(2,3), B_toe=None, slope=None, B=None, beta=0,
filter_rule=None, Soil=None, phi=40, id=None, **kwargs):
""" See help(RubbleMound) for more info """
# if not from RockRubbleMound or ConcreteRubbleMound
if (not isinstance(self, RockRubbleMound)
and not isinstance(self, ConcreteRubbleMound)):
# not called from a child class
# therefore some attributes must be set
self.logger = {'INFO': [], 'WARNING': []}
self.structure = {'armour': {'computed Dn50': Dn50,
'class': None,
'class Dn50': Dn50,
'state': None}}
self.alpha = np.arctan(slope[0]/slope[1])
self.id = id
self.variantIDs = ['a', 'b', 'c', 'd']
# set attribute of bishop and normative F
self.bishop = None
self.F_norm = None
# set input as private attribute
self._input_arguments = {
'slope': slope,
'slope_toe': slope_toe,
'B': B,
'armour': armour_layer,
'Grading': Grading,
'Dn50_core': Dn50_core
}
# check for supported armour layers, and if filter_rule is set
supported = substructure._supported_armour_layers()
if armour_layer in supported:
filter_rule = armour_layer
elif filter_rule is None:
supported_rules = ', '.join(supported)
raise NotSupportedError(
(f'Filter rule for {armour_layer} is not implemented, set '
f'filter rule to use with filter_rule to {supported_rules}'))
# set the LimitStates as private attribute to use in plot
self._LimitStates = LimitStates
# design the first underlayer of the breakwater
# rho is the density of the material of the armour layer
computed_dn_u = substructure.underlayer(
Dn_armour=Dn50, armour_layer=filter_rule, rho=rho,
rho_rock=Grading.rho)
# set empty lists to store design values
class_underlayer, class_dn_u = [], []
class_filter, class_dn_f, computed_dn_f = [], [], []
for i, dn in enumerate(computed_dn_u):
rock_class = Grading.get_class(dn)
if rock_class in class_underlayer:
# if rock class is already in the underlayer there is
# no need to generate an additional variant
continue
else:
# new design and values must thus be saved
new_underlayer = True
class_dn = Grading.get_class_dn50(rock_class)
class_underlayer.append(rock_class)
class_dn_u.append(class_dn)
# design a second underlayer/filter layer
dn_filter_range = substructure.filter_layers(
Dn=class_dn, rho=rho)
for dn_filter in dn_filter_range:
if dn_filter > Dn50_core:
# computed dn of the filter is larger than the
# core, a filter layer is thus needed
rock_class = Grading.get_class(dn_filter)
if rock_class in class_filter and not new_underlayer:
# if rock class is already in the filter layer
# there is no need to generate an additional
# variant
continue
else:
# new design and values must thus be saved
class_dn = Grading.get_class_dn50(rock_class)
computed_dn_f.append(dn_filter)
class_filter.append(rock_class)
class_dn_f.append(class_dn)
new_underlayer = False
else:
# a filter layer is not needed
if new_underlayer:
# set all values for the filter layer to None
computed_dn_f.append(None)
class_filter.append(None)
class_dn_f.append(None)
# set new_underlayer to False so that this
# action is not repeated
new_underlayer = False
else:
# still designing for the same underlayer
# so no need to set new values
continue
# add underlayer and filter to the structure
self.structure['underlayer'] = {'computed Dn50': computed_dn_u,
'class': class_underlayer,
'class Dn50': class_dn_u,
'state': 'see armour',
'layers': layers_underlayer}
if any(class_filter):
self.structure['filter layer'] = {'computed Dn50': computed_dn_f,
'class': class_filter,
'class Dn50': class_dn_f,
'state': 'see armour',
'layers': layers_underlayer}
# determine number of variants
self.variantIDs = self.variantIDs[:len(class_filter)]
# add generated variants to the logger
if len(class_underlayer) == 2:
self.logger['INFO'].append(
'two rock classes possible for the underlayer, generated new '
'variant b')
if len(class_filter) == 4:
self.logger['INFO'].append(
'two rock classes possible for the filter layer, '
'generated new variant c')
self.logger['INFO'].append(
'two rock classes possible for the filter layer, '
'generated new variant d')
if len(class_filter) == 3:
self.logger['INFO'].append(
'two rock classes possible for the filter layer, generated '
'new variant c')
# design toe of the structure
delta_rock = (Grading.rho - rho_w) / rho_w
# make a first estimate for the height of the toe
h_toe = self._estimate_htoe()
# set variables to store results from normative LimitState
Dn50_toe, state_toe = 0, 0
for i, LimitState in enumerate(LimitStates):
# get values from the LimitState
Hs = LimitState.get_Hs(definition='H13')
h = LimitState.h
Nod = LimitState['Nod']
# make first estimate for the water level above the toe
ht_estimate = h - h_toe
# use while loop since ht depends on dn50 of the toe
# first set temporary values for the while loop
Dn50_toe_temp = 0
compute_toe = True
counter = 0
while compute_toe:
Dn50_toe_computed = toe_stability(
Hs=Hs, h=LimitState.h, ht=ht_estimate,
Delta=delta_rock, Nod=Nod)
# check for convergence
if (abs(Dn50_toe_computed - Dn50_toe_temp) < 0.05
or counter > 50):
# value has converged, so break loop
compute_toe=False
# replace old value with the new one
Dn50_toe_temp = Dn50_toe_computed
# make new estimate for the water level above the toe
ht_estimate = h - self._estimate_htoe(Dn50=Dn50_toe_computed)
counter += 1
# check if computed Dn50 of current LimitState is larger
# than current normative Dn50
if Dn50_toe_temp > Dn50_toe:
# if larger the normative Dn50 must be changed
Dn50_toe = Dn50_toe_temp
state_toe = i
class_toe = Grading.get_class(Dn50_toe)
class_Dn50 = Grading.get_class_dn50(class_toe)
self.structure['toe'] = {'computed Dn50': Dn50_toe,
'class': class_toe,
'class Dn50': class_Dn50,
'state': state_toe}
# determine crest height
self.Rc, self._state_overtopping = 0, 0
# check if crest width is more than 3*Dn50 of armour
if B >= 3*Dn50:
# no action required
pass
else:
# get armour_layer
if armour_layer is 'Rock':
material = 'armourstones'
else:
material = 'units'
user_warning(
(f'Given crest width is smaller than three {material}, it is '
'advised to increase the width to at least '
f'{np.round(3*Dn50, 2)} m'))
# convert beta from deg to rad for overtopping computation
beta = beta * np.pi/180.
for i, LimitState in enumerate(LimitStates):
Hm0 = LimitState.get_Hs(definition='Hm0')
xi = LimitState.surf_similarity(
alpha=self.alpha, number='spectral')
Rc_temp = rubble_mound(
Hm0=Hm0, q=LimitState['q'], xi_m_min_1=xi, alpha=self.alpha,
beta=beta, gamma_b=1, gamma_v=1, gam_star=1, Gc=B, Dn50=Dn50,
armour_layer=armour_layer, layers=layers,
permeability='permeable', safety=safety)
# check if computed Rc of current LimitState is larger
# than current normative Rc
if Rc_temp > self.Rc:
# if larger the normative Rc must be changed
self.Rc = Rc_temp
self._state_overtopping = i
# now that the toe is designed the width of the toe can be computed
# check if a width has been given
if B_toe is None:
# set toe width to 3 times Dn50
self.B_toe = 3 * self.structure['toe']['class Dn50']
else:
# user specified width
self.B_toe = B_toe
# check if given width is larger than 3 stones
if B_toe < 3 * self.structure['toe']['class Dn50']:
user_warning(
('given width of the toe is smaller than three times the '
'Dn50 of the toe'))
# Compute required scour protection
self.width_scour = 0
for LimitState in LimitStates:
w = scour_protection(
L=LimitState.L(period='Tm'), slope=slope)
# check if larger than previous value
if w >= self.width_scour:
# set w as new width scour
self.width_scour = w
# check if a soil has been given
if Soil is not None:
# show warning as implementation is not yet verified
user_warning(
(f'The implementation of Bishop into {type(self).__name__} '
'has not yet been verified with an example'))
# check if called from ConcreteRubbleMound
if isinstance(self, ConcreteRubbleMound):
# raise warning that armour is modelled as rock
user_warning('The armour layer is modelled as Rock in Bishop')
# compute the height of the breakwater
h = LimitStates[self._state_overtopping].h + self.Rc
# compute horizontal length of the slope
x = slope[1]*h/slope[0]
# determine number of slices, with slice width of 1 m
num_slices = np.round(h, 0)
n = 0.4
# compute volumetric weights
gamma_w = rho_w*9.81/1000
gamma_r = (1-n)*Grading.rho*9.81/1000
gamma_r_sat = gamma_r + n*gamma_w
# set variable to store normative factor of safety
self.F_norm = 10*10
# iterate over the LimitStates
for LimitState in LimitStates:
# create bishop object
slip = Bishop(point2=(x,h), wlev=LimitState.h)
# add soil and rock layer
slip.add_layer(
gamma=Soil.gamma, gamma_sat=Soil.gamma_sat, c=Soil.c,
phi=Soil.phi*180/np.pi, name='Subsoil', ymin=-50, ymax=0)
slip.add_layer(
gamma=gamma_r, gamma_sat=gamma_r_sat, c=0, phi=phi,
name='Rock', ymin=0, ymax=h)
# compute factor of safety
slip.compute(num_slices=int(num_slices), gamma_w=gamma_w)
# check if normative factor of safety
if slip.circles[slip.normative].F < self.F_norm:
# update normative F and bishop object as attribute
self.F_norm = slip.circles[slip.normative].F
self.bishop = slip
def _estimate_htoe(self, Dn50=0):
""" Method to estimate the height of the toe """
# set ht variable
ht = 0
# get normative layer thickness
for i, id in enumerate(self.variantIDs):
# get structure
structure = self.get_variant(id)
# get thickness of the layer
t_armour = self._layer_thickness(
'armour', self._input_arguments['armour'], structure)
t_underlayer = self._layer_thickness(
'underlayer', 'Rock', structure)
t_filter = self._layer_thickness('filter layer', 'Rock', structure)
ht_est = t_underlayer + t_filter
if Dn50 != 0:
ht_est += np.ceil(t_armour/Dn50) * Dn50
# check if larger than previous estimate
if ht_est > ht:
ht = ht_est
return ht
def _validate_variant(self, variants):
""" Validate the input of the variant
Parameters
----------
variants : tuple
variantIDs given as args input
"""
# check if a variant is specified
if not variants:
# no input is given so get the valid args for variant
valid_args = self.variantIDs
valid_args.append('all')
# raise error
valid = ', '.join(valid_args)
raise InputError(
'did not specify which variants to plot, possible arguments '
f'are {valid}')
# check if input all is in variants
if 'all' in variants:
# set specified variants to all variantIDs
variants = tuple(self.variantIDs)
# return the variants
return variants
def _layer_thickness(self, layer, material, structure):
""" Compute the thickness of the layer
Parameters
----------
layer : {armour, underlayer, filter layer}
name of the layer for which the layer thickness must be
computed
material : str
material of which the layer is made
structure : dict
parameters and values (Dn50, Rock class) for one variant
Returns
-------
float
thickness of the layer [m]
"""
# check if layer is in structure
if layer in structure:
# get number of layers and Dn50 from the structure
layers = structure[layer]['layers']
Dn50 = structure[layer]['class Dn50']
# get the layer coefficient
kt = substructure.layer_coefficient(
material, layers=layers, placement='standard')
# compute and return the thickness of the layer
return kt*layers*Dn50
else:
# set layer thickness to 0, as layer is not in the structure
return 0
def _layers(self, variantID):
""" compute the coordinates of all layers
Parameters
----------
variantID : str
identifier of the variant, see :py:attr:`variantIDs` for a
list of all generated variants.
Returns
-------
dict
coordinates of all layers
"""
# get structure of the current variant
structure = self.get_variant(variantID)
# set empty dict to store coordinates in
coordinates = {}
# get the slope
V, H = self._input_arguments['slope']
V_toe, H_toe = self._input_arguments['slope_toe']
# compute constant to transformthickness of layer to x and
# y coordinates, switched V and H because orthogonality
transform_x = V/np.sqrt(V**2+H**2)
transform_y = H/np.sqrt(V**2+H**2)
# get the height and width of the structure
height = self._LimitStates[self._state_overtopping].h + self.Rc
B = self._input_arguments['B']
# determine thickness of the layers
t_armour = self._layer_thickness(
'armour', self._input_arguments['armour'], structure)
t_underlayer = self._layer_thickness('underlayer', 'Rock', structure)
t_filter = self._layer_thickness('filter layer', 'Rock', structure)
if t_filter == 0:
# add scour protection below underlayer on sea side
t_scour = 2*self._input_arguments['Dn50_core']
else:
# filter layer will be used as scour protection
t_scour = 0
# compute armour layer
armour_y1 = t_filter + t_underlayer + t_scour
armour_y2 = height
armour_y3 = height
armour_x1 = -0.5*B - H*(armour_y3-armour_y1)/V
armour_x2 = -0.5*B
armour_x3 = 0.5*B
armour_x4 = 0.5*B + H*armour_y3/V
# compute line between armour and underlayer
arm_under_y1 = t_filter + t_underlayer + t_scour
arm_under_y2 = t_filter + t_underlayer + t_scour
arm_under_y3 = height - t_armour
arm_under_y4 = height - t_armour
arm_under_x5 = (armour_x4 - H*(t_armour * transform_y)/V
- t_armour*transform_x)
arm_under_x4 = arm_under_x5 - H*arm_under_y4/V
arm_under_x3 = -arm_under_x4
arm_under_x2 = -H*(arm_under_y3 - arm_under_y2)/V + arm_under_x3
# compute lower line of the underlayer
under_y1 = t_filter + t_scour
under_y2 = t_filter + t_scour
under_y3 = height - t_armour - t_underlayer
under_y4 = height - t_armour - t_underlayer
under_x5 = (arm_under_x5 - H*(t_underlayer * transform_y)/V
- t_underlayer*transform_x)
under_x4 = under_x5 - H*under_y4/V
under_x3 = -under_x4
under_x2 = -H*(under_y3 - under_y2)/V + under_x3
# compute the toe
Dn50 = structure['toe']['class Dn50']
# check if armour layer is made out of rock
# as there is a different toe structure for rock and armour units
if self._input_arguments['armour'] is 'Rock':
# compute point where armour layer intersect with the toe
x = ((abs(arm_under_x2 - armour_x1) * V_toe/H_toe)
/ (V/H + V_toe/H_toe))
y = abs(arm_under_x2 - armour_x1)*V*V_toe/(H*V_toe + V*H_toe)
# determine height of the toe
htoe = np.ceil(y/Dn50) * Dn50
toe_low = t_underlayer + t_filter + t_scour
toe_top = toe_low + htoe
toe_x4 = arm_under_x2
toe_x3 = toe_x4 - H_toe*htoe/V_toe
toe_x2 = toe_x3 - self.B_toe
toe_x1 = (toe_x2 - H_toe*htoe/V_toe)
# change first points of the armour layer
# so that the armour layer starts at the intersection
armour_x1 = armour_x1 + x
armour_y1 = armour_y1 + y
else:
# armour units
htoe = np.ceil(t_armour/Dn50) * Dn50
toe_low = t_underlayer + t_filter + t_scour
toe_top = toe_low + htoe
toe_x4 = armour_x1
toe_x3 = armour_x1 + H*htoe/V
toe_x2 = toe_x3 - self.B_toe
toe_x1 = (toe_x2 - H_toe*htoe/V_toe)
# check if there is a filter layer
if t_filter != 0:
# set points of end of the layers
arm_under_x1 = toe_x1 - 3*structure['underlayer']['class Dn50']
arm_under_x0 = arm_under_x1 - H*t_underlayer/V
arm_under_y0 = t_filter
# compute the lower line of the filter layer
filter_y3 = height - t_armour - t_underlayer - t_filter
filter_y4 = height - t_armour - t_underlayer - t_filter
filter_x5 = (under_x5 - H*(t_filter * transform_y)/V
- t_filter*transform_x)
filter_x4 = filter_x5 - H*filter_y4/V
filter_x3 = -filter_x4
filter_x2 = -H*filter_y3/V + filter_x3
filter_x1 = arm_under_x0 - self.width_scour
filter_x0 = filter_x1 - t_filter*H/V
else:
# no filter layer
arm_under_x1 = toe_x1 - 3*structure['underlayer']['class Dn50']
arm_under_x0 = arm_under_x1 - H*t_underlayer/V
arm_under_y0 = t_scour
# add scour protection
scour_x2 = arm_under_x0 - self.width_scour
scour_x1 = scour_x2 - t_scour*H/V
# add lines to the coordinates
# are added from left to right and back
coordinates['armour'] = {
'x': [armour_x1, armour_x2, armour_x3, armour_x4, arm_under_x5,
arm_under_x4, arm_under_x3, arm_under_x2, armour_x1],
'y': [armour_y1, armour_y2, armour_y3, 0, 0,
arm_under_y4, arm_under_y3, arm_under_y2, armour_y1]}
coordinates['underlayer'] = {
'x': [arm_under_x0, arm_under_x1, arm_under_x2, arm_under_x3,
arm_under_x4, arm_under_x5, under_x5, under_x4, under_x3,
under_x2, arm_under_x0],
'y': [arm_under_y0, arm_under_y1, arm_under_y2, arm_under_y3,
arm_under_y4, 0, 0, under_y4, under_y3,
under_y2, arm_under_y0]}
# check if there is a filter to add to coordinates
if t_filter != 0:
# add filter to coordinates
coordinates['filter layer'] = {
'x': [filter_x0, filter_x1, under_x2, under_x3, under_x4,
under_x5, filter_x5, filter_x4, filter_x3, filter_x2,
filter_x0],
'y': [0, t_filter, under_y2, under_y3, under_y4,
0, 0, filter_y4, filter_y3, 0, 0]}
# add core to coordinates
coordinates['core'] = {
'x': [filter_x2, filter_x3, filter_x4, filter_x5, filter_x2],
'y': [0, filter_y3, filter_y4, 0, 0]
}
else:
coordinates['core'] = {
'x': [scour_x1, scour_x2, under_x2, under_x3, under_x4,
under_x5, scour_x1],
'y': [0, t_scour, t_scour, under_y3, under_y4,
0, 0]
}
# add the coordinates of the toe
coordinates['toe'] = {
'x': [toe_x1, toe_x2, toe_x3, toe_x4, toe_x1],
'y': [toe_low, toe_top, toe_top, toe_low, toe_low]}
# return the coordinates of the specified variants
return coordinates
def _cost(
self, *variants, core_price, unit_price, transport_cost,
output='variant'):
""" Compute the cost per meter for each variant
Method to compute the cost of each generated variant, the cost
is computed per meter
Parameters
----------
*variants : str
IDs of the variants to plot, see :py:attr:`variantIDs` for
a list of all generated variants. If 'all' is in the
arguments, all variants will be plotted.
core_price : float
cost of the core material per m³
unit_price : float
the cost of an armour unit per m³
transport_cost : float
the cost to transport a m³ of rock from the quarry to the
project location
output : {variant, layer, average}
format of the output dict, variant returns the total cost
of each variant, layer the cost of each layer for each
variant and average returns the average cost.
Returns
-------
dict
the cost
Raises
------
RockGradingError
if no pricing is included in the given RockGrading
"""
# check if transport cost have been given
if transport_cost is None:
# no cost thus set cost to zero
transport_cost = 0
# validate variants
variants = self._validate_variant(variants)
# get the grading, and check if the cost has been added
Grading = self._input_arguments['Grading']
if 'price' in Grading[list(Grading.grading.keys())[0]]:
# pricing has been added
pass
else:
# pricing has not been added, raise error
raise RockGradingError('There is no pricing in the RockGrading')
# set empty dict to store the output in
cost = {}
# iterate over the generated variants
for id in variants:
# get the areas and structure of the variants
areas = self.area(id)
structure = self.get_variant(id)
# iterate over the layers to price each layer
variant_price = {}
for layer, area in areas.items():
if layer is 'core':
# core is not included in the structure dict
price = ((core_price + transport_cost)
* self._input_arguments['Dn50_core'])
elif (self._input_arguments['armour'] is not 'Rock'
and layer is 'armour'):
# concrete armour units
price = area * unit_price
else:
# layer of the breakwater
rock_class = structure[layer]['class']
# get the price per meter
price = ((Grading[rock_class]['price'] + transport_cost)
* area)
# add to dict
variant_price[layer] = np.round(price, 2)
# add to cost dict
if output is 'variant' or output is 'average':
# add total cost of all layers
cost[id] = np.round(np.sum(list(variant_price.values())), 2)
elif output is 'layer':
# add the cost of each layer
cost[id] = variant_price
else:
# invalid input
raise NotSupportedError(
(f'Cost can\'t be exported as {output}, must be variant, '
'layer or average'))
# check if average must be computed
if output is 'average':
# compute average cost
cost = {'average': np.round(np.average(list(cost.values())), 2)}
# check if the cost have only been computed for 1 variant
if len(variants) == 1:
# print user_warning and change key into variant
cost[variants[0]] = cost.pop('average')
user_warning(
('Computing the average for one variantID, changed key '
'average in dict with the specified variantID'))
return cost
def get_variant(self, variantID):
""" Get the dimensions for the specified variant
Parameters
----------
variantID : str
identifier of the variant, see :py:attr:`variantIDs` for a
list of all generated variants.
Returns
-------
dict
Parameters and values (Dn50, Rock class) for one variant
Raises
------
KeyError
If there is no variant with the given identifier
"""
variant = {'armour': {}, 'underlayer': {}}
VariantCount = len(self.variantIDs)
if variantID in self.variantIDs:
if variantID == 'a':
key_u = 0
key_f = 0
elif variantID == 'b':
if VariantCount == 4:
key_u = 0
key_f = 1
elif 'filter layer' in self.structure:
key_f = 1
if VariantCount == 2:
key_u = 0
else:
key_u = 1
elif VariantCount == 2:
key_u = 1
elif variantID == 'c':
key_u = 1
if VariantCount >= 3:
key_f = 2
elif variantID == 'd':
key_u = 1
key_f = 3
else:
raise KeyError(f'Variant with ID = {variantID} is not a variant, '
f'generated variants are: {self.variantIDs}')
# add armour layer
variant['armour'] = self.structure['armour']
# add underlayer
underlayer = self.structure['underlayer']
for param in underlayer:
if param == 'state' or param == 'layers':
variant['underlayer'][param] = underlayer[param]
else:
variant['underlayer'][param] = underlayer[param][key_u]
# add filter layer
if 'filter layer' in self.structure:
filter_layer = self.structure['filter layer']
variant['filter layer'] = {}
for i, param in enumerate(filter_layer):
# check because values of these are not list
if param == 'state' or param == 'layers':
# no need to use key_f, because constant for all
variant['filter layer'][param] = filter_layer[param]
else:
# param is list and thus use key_f to get correct value
variant['filter layer'][param] = filter_layer[param][key_f]
# check if valid filter layer or all None
# slice because last two are state and layers
sliced_values = list(variant['filter layer'].values())[:-2]
if not any(sliced_values):
# all values are None, thus delete filter layer
del variant['filter layer']
# add toe
if 'toe' in self.structure:
variant['toe'] = self.structure['toe']
return variant
def plot(self, *variants, wlev=None, save_name=None):
""" Plot the cross section of the specified breakwater(s)
Parameters
----------
*variants : str
IDs of the variants to plot, see :py:attr:`variantIDs` for
a list of all generated variants. If 'all' is in the
arguments, all variants will be plotted.
wlev : str, optional, default: None
label of the :py:class:`LimitState` from which the water
level will be plotted. If no value is specified the water
level from the normative limit state is used, which is the
normative LimitState from the crest freeboard computation.
save_name : str, optional, default: None
if given the cross section is not shown but saved with the
given name
Raises
------
InputError
If no variants are specified or if the label of wlev is not
a valid label of a :py:class:`LimitState`
KeyError
If there is no variant with the given identifier
"""
# validate variants
variants = self._validate_variant(variants)
if wlev is None:
wlev = self._state_overtopping
else:
for i, LimitState in enumerate(self._LimitStates):
if LimitState.label == wlev:
wlev = i
break
# check if wlev has been changed from LimitState label into index
if isinstance(wlev, str):
# wlev is still a string so not changed, which means that
# the specified wlev is not a specified LimitState
raise InputError('There is no LimitState with the given label')
V, H = self._input_arguments['slope']
plt.figure(figsize=(10, 5))
for i, id in enumerate(variants):
# set subplot
if len(variants) == 2:
plt.subplot(1, 2, i+1)
elif len(variants) >= 3:
plt.subplot(2, 2, i+1)
# get the coordinates
coordinates = self._layers(id)
# set xlim_max and xlim_min variable
xlim_max, xlim_min = 0, 0
# plot lines
for layer, lines in coordinates.items():
plt.plot(lines['x'], lines['y'], color='k')
# check largest value for xlim
if np.max(lines['x']) >= xlim_max:
# set max as xlim_max
xlim_max = np.max(lines['x'])
# check smallest value for xlim
if np.min(lines['x']) <= xlim_min:
# set min as xlim_min
xlim_min = np.min(lines['x'])
# bottom + wlev
x_wlev_max = 0.5*self._input_arguments['B'] + H*self.Rc/V
plt.axhline(y=0, color='k', linewidth=2)
plt.hlines(
y=self._LimitStates[wlev].h, xmin=xlim_min*1.2,
xmax=-x_wlev_max, color='b')
plt.hlines(
y=self._LimitStates[wlev].h, xmin=x_wlev_max,
xmax=xlim_max*1.2, color='b')
# set xlim and ylim
ymax = (self._LimitStates[self._state_overtopping].h + self.Rc)*1.2
plt.xlim(xlim_min*1.2, xlim_max*1.2)
plt.ylim(-0.5, ymax)
# add title to the plot
if save_name is None:
if isinstance(self.id, int):
title = ('Cross section of rubble mound breakwater '
f'{self.id}{id}')
else:
title = f'Cross section of rubble mound breakwater {id}'
else:
name = save_name.split('/')[-1]
title = f'Cross section of {name}'
plt.title(title)
plt.gca().set_aspect('equal', adjustable='box')
plt.grid()
plt.tight_layout()
if save_name is not None:
plt.savefig(f'{save_name}.png')
plt.close()
else:
plt.show()
def print_variant(self, *variants, decimals=3):
""" Print the details for the specified variant(s)
This method will print the computed Dn50, rock class, average
Dn50 of the class, normative LimitState for all layers and
specified variants.
Parameters
----------
*variants : str
IDs of the variants to plot, see :py:attr:`variantIDs` for
a list of all generated variants. If 'all' is in the
arguments, all variants will be plotted.
decimals : int, optional, default: 3
number of decimal places to round to
Raises
------
InputError
If no arguments are specified
KeyError
If there is no variant with the given identifier
"""
# validate variants
variants = self._validate_variant(variants)
# loop over the variants
for id in variants:
variant = self.get_variant(id)
# set the name of the table
if isinstance(self.id, int):
table_name = f' Variant {self.id}{id}'
else:
table_name = f' Variant {id}'
print(table_name)
table = []
# iterate over the layers
for i, (layer, dimensions) in enumerate(variant.items()):
if i == 0:
# set headers of the table
headers = list(dimensions.keys())
headers.insert(0, 'Layer')
# add empty list for each row (layer)
table.append([])
table[i].append(layer)
table[i].extend(dimensions.values())
# print the label of the LimitState instead of the index
state = dimensions['state']
index_state = headers.index('state')
if isinstance(table[i][index_state], int):
table[i][index_state] = self._LimitStates[state].label
print(tabulate(table, headers, tablefmt="github",
floatfmt=(f'.{decimals}f')))
print('')
print(f'Rc = {np.round(self.Rc, decimals=decimals)} m, designed '
f'with {self._LimitStates[self._state_overtopping].label} '
'limit state')
print('\n')
def print_logger(self, level='warnings'):
""" Print messages and warnings from the logger
Parameters
----------
msg_level : {'info', 'warnings'}, optional, default: 'warnings'
specify print level, highest level is warnings and lowest
level is info. Note that the info level will also print all
warnings
"""
# check if correct input has been given
if level.lower() not in ['warnings', 'info']:
raise NotSupportedError(
f'{level} not implemented, must be info or warnings')
# print logger
for type, messages in self.logger.items():
if level.lower() == 'warnings':
if type == 'INFO':
continue
print(f'{type}:')
if messages:
for message in messages:
print(message)
else:
print(f'no {type.lower()} messages in log')
print()
def area(self, variantID):
""" Compute the area of all layers
Method computes the area of each layer using Gauss's area
formula. Which is given by the following formula:
.. math::
\\mathbf{A}=\\frac{1}{2} | \\sum_{i=1}^{n-1} x_{i} y_{i+1}
+x_{n} y_{1}-\\sum_{i=1}^{n-1} x_{i+1} y_{i}-x_{1} y_{n} |
Parameters
----------
variantID : str
identifier of the variant, see :py:attr:`variantIDs` for a
list of all generated variants.
Returns
-------
dict
dict with the area of each layer
"""
# get the coordinates of the layers
coordinates = self._layers(variantID)
# iterate over the layers to compute the area
area = {}
for layer, coord in coordinates.items():
# get the x and y coordinates
x = coord['x']
y = coord['y']
# use Gauss's area formula
A = 0.5*np.abs(np.dot(x,np.roll(y,1))-np.dot(y,np.roll(x,1)))
# add to area dict
area[layer] = A
return area
[docs]class RockRubbleMound(RubbleMound):
""" Design a breakwater with Rock as armour layer
Makes a conceptual design for a conventional rubble mound breakwater
with rock as the armour layer, for one or several limit states. The
following computations are performed:
- The armour layer is designed with the Van der Meer formulas for
deep and shallow water (van der Meer, 1988; van Gent et al., 2003).
- The underlayer is designed by using the rules for the underlayer
- A filter layer is designed if one is needed, depends on
:py:obj:`Dn50_core`
- The toe is designed with the toe stability formula of
Van der Meer (1998).
- The crest freeboard is computed with the formula from EurOtop
(2018)
- The required width of the scour protection with Sumer and Fredsoe
(2000)
- If a :py:class:`Soil` is specified, a slip circle analysis is
performed with :py:class:`Bishop`
.. note::
Depending on the input it might be that more rock classes are
possible for the underlayer (and filter layer). In case the
upper bound of the underlayer rule results in a different rock
class as the lower bound, a new variant is generated. See
:py:attr:`variantIDs` for a list of generated variants.
.. note::
The notional permeability, P, in the van der Meer formula is set
to a constant value of 0.4. This is due to the fact that the
substructure of the breakwater will always have an underlayer.
Parameters
----------
slope : tuple
Slope of the armour layer (V, H). For example a slope of 3V:4H
is defined as (3, 4)
slope_foreshore : tuple
slope of the foreshore (V, H). For example a slope of 1:100 is
defined as (1, 100)
rho_w : float
density of water [kg/m³]
B : float
Crest width [m]
N : int
Number of incident waves at the toe of the structure [-]
LimitState : :py:class:`LimitState` or list of :py:class:`LimitState`
ULS, SLS or another limit state defined with
:py:class:`LimitState`
Grading : :py:class:`RockGrading`
standard rock grading defined in the NEN-EN 13383-1 or a user
defined rock grading
Dn50_core : float
nominal diameter for the stones in the core of the breakwater [m]
safety : float, optional, default: 1
safety factor of design (number of standard deviations from the
mean)
slope_toe : tuple, optional, default: (2,3)
slope of the toe
B_toe : float, optional, default: None
width of the top of the toe in meters. By default the width of
toe is taken as 3 * Dn50_toe.
beta : float, optional, default: 0
angle between direction of wave approach and a line normal to
the breakwater (degrees).
layers : int, optional, default: 2
number of layers in the armour layer
layers_underlayer : int, optional, default: 2
number of layers in the underlayer
vdm : {min, max, avg}, optional, default: max
value to return in case both the deep and shallow water formula
are valid. min for the lowest value, max for the highest value
and avg for the average value, default is max.
Soil : :py:class:`Soil`, optional, default: None
by default Soil is None, which means that the geotechnical checks
are not performed. By specifying a Soil object, the geotechnical
checks are automatically performed.
phi : float, optional, default: 40
internal friction angle of rock [degrees]
id : int, optional, default: None
add a unique id to the breakwater
Attributes
----------
logger : dict
dict of warnings and messages
structure : dict
dictionary with the computed Dn50, rock class, and average Dn50
of the rock class for each layer and the toe. This dictionary
includes all variants, use :py:meth:`get_variant` to get the
parameters of one specific variant. Alternatively,
:py:meth:`print_variant` can be used to print the details of
one, multiple or all variants.
alpha : float
slope of the structure in radians
id : int
unique id of the breakwater
variantIDs : list
list with the IDs of the variants generated for this rubble
mound breakwater.
Rc : float
the crest freeboard of the structure [m]
width_scour : float
the required length of the scour protection [m]
"""
def __init__(
self, slope, slope_foreshore, rho_w, B, N, LimitState, Grading,
Dn50_core, safety=1, slope_toe=(2,3), B_toe=None, beta=0, layers=2,
layers_underlayer=2, vdm='max', Soil=None, phi=40, id=None,
**kwargs):
""" See help(RockRubbleMound) for more info """
# set logger and structure
self.logger = {'INFO': [], 'WARNING': []}
self.structure = {}
# compute angles
self.alpha = np.arctan(slope[0]/slope[1])
slope_foreshore = np.arctan(slope_foreshore[0]/slope_foreshore[1])
# set id and variantIDs
self.id = id
self.variantIDs = ['a', 'b', 'c', 'd']
# compute relative buoyant density
delta_rock = (Grading.rho - rho_w)/rho_w
# set attribute for vandermeer to check validity
# notional permeability is fixed due to substructure
self._vandermeer = {
'N': N, 'P': 0.4, 'Delta': delta_rock}
# convert single LimitState to list if needed
if isinstance(LimitState, list):
LimitStates = LimitState
else:
LimitStates = [LimitState]
# set temporary values to check changes
Dn50, state = 0, 0
for i, LimitState in enumerate(LimitStates):
# design armour layer
Dn50_temp = vandermeer(
LimitState=LimitState, Delta=delta_rock, P=0.4, N=N,
alpha=self.alpha, slope_foreshore=slope_foreshore,
val=vdm, safety=safety, logger=self.logger)
# check if computed Dn50 of current LimitState is larger
# than current normative Dn50
if Dn50_temp > Dn50:
# if larger the normative Dn50 must be changed
Dn50 = Dn50_temp
state = i
class_armour = Grading.get_class(Dn50)
# check dn85/dn15 range with rosin_rammler
M15 = Grading.rosin_rammler(class_=class_armour, y=0.15)
dn15 = (M15/Grading.rho)**(1/3)
M85 = Grading.rosin_rammler(class_=class_armour, y=0.85)
dn85 = (M85/Grading.rho)**(1/3)
self._vandermeer['gradation'] = dn85/dn15
class_Dn50 = Grading.get_class_dn50(class_armour)
self.structure['armour'] = {'computed Dn50': Dn50,
'class': class_armour,
'class Dn50': class_Dn50,
'state': state,
'layers': layers}
# design underlayer, filter layer and crest height
super().__init__(
Dn50=class_Dn50, Dn50_core=Dn50_core, rho=Grading.rho,
rho_w=rho_w, armour_layer='Rock', layers=layers,
LimitStates=LimitStates, Grading=Grading, safety=safety,
layers_underlayer=layers_underlayer, slope_toe=slope_toe,
B_toe=B_toe, slope=slope, B=B, beta=beta, id=id, Soil=Soil,
phi=phi, **kwargs)
def __str__(self):
return (f'id.{self.id}: breakwater with rock as armour layer, '
f'and variants: {self.variantIDs}')
[docs] def cost(
self, *variants, core_price, transport_cost=None,
output='variant'):
""" Compute the cost per meter for each variant
Method to compute the cost of each generated variant, the cost
is computed per meter. The cost of the rocks must be specified
in the RockGrading. If transport cost are not included in the
price of rocks or core_price it can be given with the argument
transport_cost.
Parameters
----------
*variants : str
IDs of the variants to plot, see :py:attr:`variantIDs` for
a list of all generated variants. If 'all' is in the
arguments, all variants will be plotted.
core_price : float
cost of the core material per m³
transport_cost : float, optional, default: None
the cost to transport a m³ of rock from the quarry to the
project location
output : {variant, layer, average}
format of the output dict, variant returns the total cost
of each variant, layer the cost of each layer for each
variant and average returns the average cost.
Returns
-------
dict
the cost
Raises
------
RockGradingError
if no pricing is included in the given RockGrading
"""
# compute the cost of the concept
cost = self._cost(
*variants, core_price=core_price, unit_price=0,
transport_cost=transport_cost, output=output)
return cost
[docs] def check_validity(self, decimals=3):
""" Check if the used parameters are within the validity range
The Van der Meer equations for deep and shallow water are
empirical equations, meaning that they are based on experiments.
Therefore, the Van der Meer equations are, strictly speaking,
only valid if the parameters are within the range of the
parameters from the experiments. This method prints a table from
which the validity range, used value and if the parameter is
within validity range can be read.
Parameters
----------
decimals : int, optional, default: 3
number of decimals
"""
# set table
headers = [
'Parameter ', 'Validity range', 'Used value', 'in range?']
table_vdm, table_toe = [], []
# get the normative LimitState
state = self.structure['armour']['state']
state_toe = self.structure['toe']['state']
LimitState = self._LimitStates[state]
# check the validity ranges of the van der meer formula
for info in self.logger['INFO']:
if LimitState.label in info:
# msg from the logger with the normative LimitState
if 'vandermeer_deep' in info:
# vandermeer_deep was used
print((' Range of validity of parameters in deep water '
'formulae by Van der Meer'))
# check the slope of the structure
slope = self._input_arguments['slope']
table_vdm.append(
['tan(alpha)', '1:6 - 1:1.5',
f'{slope[0]}:{slope[1]}'])
if slope[0]/slope[1] >= 1/6 and slope[0]/slope[1] <= 1/1.5:
table_vdm[0].append('yes')
else:
table_vdm[0].append('NO')
# check the number of waves
N = self._vandermeer['N']
table_vdm.append(['N', '< 7500', np.round(N, decimals)])
if N < 7500:
table_vdm[1].append('yes')
else:
table_vdm[1].append('NO')
# check wave steepness s_om (based on Tm)
s_om = LimitState.s(number='mean')
table_vdm.append(
['s_om', '0.01 - 0.06', np.round(s_om, decimals)])
if s_om >= 0.01 and s_om <= 0.06:
table_vdm[2].append('yes')
else:
table_vdm[2].append('NO')
# surf_similarity parameter based on Tm
xi_m = LimitState.surf_similarity(
alpha=self.alpha, number='mean')
table_vdm.append(
['xi_m', '0.7 - 7', np.round(xi_m, decimals)])
if xi_m >= 0.7 and xi_m <= 7:
table_vdm[3].append('yes')
else:
table_vdm[3].append('NO')
# delta
delta = self._vandermeer['Delta']
table_vdm.append(
['Delta', '1 - 2.1', np.round(delta, decimals)])
if delta >= 1 and delta <= 2.1:
table_vdm[4].append('yes')
else:
table_vdm[4].append('NO')
# relative water depth at the toe
Hs = LimitState.get_Hs(
definition='H13')
h = LimitState.h
table_vdm.append(['h/Hs', '> 3', np.round(h/Hs, decimals)])
if h/Hs > 3:
table_vdm[5].append('yes')
else:
table_vdm[5].append('NO')
# notional permeability
P = self._vandermeer['P']
table_vdm.append(['P', '0.1 - 0.6', np.round(P, decimals)])
if P >= 0.1 and P <= 0.6:
table_vdm[6].append('yes')
else:
table_vdm[6].append('NO')
# armourstone gradation
gradation = self._vandermeer['gradation']
table_vdm.append(
['Dn85/Dn15', '< 2.5', np.round(gradation, decimals)])
if gradation < 2.5:
table_vdm[7].append('yes')
else:
table_vdm[7].append('NO')
# damage-storm duration ratio
Sd = LimitState['Sd']
damage_ratio = Sd/np.sqrt(N)
table_vdm.append(
['Sd/sqrt(N)', '< 0.9',
np.round(damage_ratio, decimals)])
if damage_ratio < 0.9:
table_vdm[8].append('yes')
else:
table_vdm[8].append('NO')
# stability number
Dn50 = self.structure['armour']['computed Dn50']
stability = Hs/(delta*Dn50)
table_vdm.append(
['Hs/(Delta*Dn50)', '1 - 4',
np.round(stability, decimals)])
if stability >= 1 and stability <= 4:
table_vdm[9].append('yes')
else:
table_vdm[9].append('NO')
# damage level parameter
table_vdm.append(['Sd', '1 - 20', np.round(Sd, decimals)])
if Sd > 1 and Sd < 30:
table_vdm[10].append('yes')
else:
table_vdm[10].append('NO')
elif 'vandermeer_shallow' in info:
# vandermeer_shallow was used
print((' Range of validity of parameters in shallow water '
'formulae by Van der Meer'))
# check the slope of the structure
slope = self._input_arguments['slope']
table_vdm.append(
['tan(alpha)', '1:4 - 1:2',
f'{slope[0]}:{slope[1]}'])
if slope[0]/slope[1] >= 1/4 and slope[0]/slope[1] <= 1/2:
table_vdm[0].append('yes')
else:
table_vdm[0].append('NO')
# check the number of waves
N = self._vandermeer['N']
table_vdm.append(['N', '< 3000', np.round(N, decimals)])
if N < 3000:
table_vdm[1].append('yes')
else:
table_vdm[1].append('NO')
# check wave steepness s_om (based on Tm)
s_om = LimitState.s(number='mean')
table_vdm.append(
['s_om', '0.01 - 0.06', np.round(s_om, decimals)])
if s_om >= 0.01 and s_om <= 0.06:
table_vdm[2].append('yes')
else:
table_vdm[2].append('NO')
# surf_similarity parameter based on Tm
xi_m = LimitState.surf_similarity(
alpha=self.alpha, number='mean')
table_vdm.append(
['xi_m', '1 - 5', np.round(xi_m, decimals)])
if xi_m >= 1 and xi_m <= 5:
table_vdm[3].append('yes')
else:
table_vdm[3].append('NO')
# surf_similarity parameter based on Tm-1.0
xi_m_min_1 = LimitState.surf_similarity(
alpha=self.alpha, number='spectral')
table_vdm.append(
['xi_m_min_1', '1.3 - 6.5',
np.round(xi_m_min_1, decimals)])
if xi_m_min_1 >= 1.3 and xi_m_min_1 <= 6.5:
table_vdm[4].append('yes')
else:
table_vdm[4].append('NO')
# wave height ratio
Hs = LimitState.get_Hs(
definition='H13')
wave_ratio = LimitState['H2_per']/Hs
table_vdm.append(
['H2%/Hs', '1.2 - 1.4', np.round(wave_ratio, decimals)])
if wave_ratio >= 1.2 and wave_ratio <= 1.4:
table_vdm[5].append('yes')
else:
table_vdm[5].append('NO')
# Deep-water wave height ratio
h = LimitState.h
table_vdm.append(['Ho/h', '0.25 - 1.5'])
try:
deep_water_ratio = LimitState['Ho']/h
table_vdm[6].append(np.round(deep_water_ratio, decimals))
if deep_water_ratio >= 0.25 and deep_water_ratio <= 1.5:
table_vdm[6].append('yes')
else:
table_vdm[6].append('NO')
except KeyError:
table_vdm[6].append(f'Ho not in {LimitState.label}')
# armourstone gradation
gradation = self._vandermeer['gradation']
table_vdm.append(
['Dn85/Dn15', '1.4 - 2.0',
np.round(gradation, decimals)])
if gradation >= 1.4 and gradation <= 2:
table_vdm[7].append('yes')
else:
table_vdm[7].append('NO')
# core material - armour ratio
Dn50 = self.structure['armour']['computed Dn50']
Dn50_core = self._input_arguments['Dn50_core']
ratio_core = Dn50_core/Dn50
table_vdm.append(
['Dn50-core/Dn50', '0 - 0.3',
np.round(ratio_core, decimals)])
if ratio_core <= 0.3:
table_vdm[8].append('yes')
else:
table_vdm[8].append('NO')
# stability number
delta = self._vandermeer['Delta']
stability = Hs/(delta*Dn50)
table_vdm.append(
['Hs/(Delta*Dn50)', '0.5 - 4.5',
np.round(stability, decimals)])
if stability >= 0.5 and stability <= 4.5:
table_vdm[9].append('yes')
else:
table_vdm[9].append('NO')
# damage level parameter
Sd = LimitState['Sd']
table_vdm.append(['Sd', '< 30', Sd])
if Sd < 30:
table_vdm[10].append('yes')
else:
table_vdm[10].append('NO')
# check validity ranges of toe formula
# check ratio water depth above toe over water depth
Dn50_toe = self.structure['toe']['computed Dn50']
htoe = np.ceil(self._estimate_htoe()/Dn50_toe) * Dn50_toe
ht = h - htoe
ratio_depth_toe = ht/h
table_toe.append(
['ht/h', '0.4 - 0.9', np.round(ratio_depth_toe, decimals)])
if ratio_depth_toe >= 0.4 and ratio_depth_toe <= 0.9:
table_toe[0].append('yes')
else:
table_toe[0].append('NO')
# ratio ht over Dn50 of the toe
ratio_dn = ht/Dn50_toe
table_toe.append(['ht/Dn50', '3 - 25', np.round(ratio_dn, decimals)])
if ratio_dn >= 3 and ratio_dn <= 25:
table_toe[1].append('yes')
else:
table_toe[1].append('NO')
# print the table
print(tabulate(table_vdm, headers, tablefmt="github"))
print()
print(' Range of validity of parameters in toe formula')
print(tabulate(table_toe, headers, tablefmt="github"))
[docs]class ConcreteRubbleMound(RubbleMound):
""" Design a breakwater with concrete armour units as armour layer
Makes a conceptual design for a conventional rubble mound breakwater
with armour units as the armour layer, for one or several limit
states. The following computations are performed:
- The armour layer is designed with the Hudson formula (Hudson, 1959),
with :math:`H_{1/3}` as wave height, in accordance with the design
guidelines for Xbloc and XblocPlus (Delta Marine Consultants, 2018).
- The underlayer is designed by using the rules for the underlayer
- A filter layer is designed if one is needed, depends on
:py:obj:`Dn50_core`
- The toe is designed with the toe stability formula of
Van der Meer (1998).
- The crest freeboard is computed with the formula from EurOtop
(2018)
- The required width of the scour protection with Sumer and Fredsoe
(2000)
- If a :py:class:`Soil` is specified, a slip circle analysis is
performed with :py:class:`Bishop`
.. note::
Depending on the input it might be that more rock classes are
possible for the underlayer (and filter layer). In case the
upper bound of the underlayer rule results in a different rock
class as the lower bound, a new variant is generated. See
:py:attr:`variantIDs` for a list of generated variants.
.. warning::
Currently only the rules for the underlayer of Rock, Xbloc and
XblocPlus have been implemented. However, it is possible to
design with another type of armour units. In this case the filter
rule must manually be set to Rock, Xbloc or XblocPlus.
Parameters
----------
slope : tuple
Slope of the armour layer (V, H). For example a slope of 3V:4H
is defined as (3, 4)
slope_foreshore : tuple
slope of the foreshore (V, H). For example a slope of 1:100 is
defined as (1, 100)
B : float
Crest width [m]
rho_w : float
density of water [kg/m³]
LimitState : :py:class:`LimitState` or list of :py:class:`LimitState`
ULS, SLS or another limit state defined with
:py:class:`LimitState`
ArmourUnit : obj
armour unit class which inherits from :py:class:`ConcreteArmour`,
for instance :py:class:`Xbloc` or :py:class:`XblocPlus`
Grading : :py:class:`RockGrading`
standard rock grading defined in the NEN-EN 13383-1 or a user
defined rock grading
Dn50_core : float
nominal diameter for the stones in the core of the breakwater [m]
safety : float, optional, default: 1
safety factor of design (number of standard deviations from the
mean)
slope_toe : tuple, optional, default: (2,3)
slope of the toe
B_toe : float, optional, default: None
width of the top of the toe in meters. By default the width of
toe is taken as 3 * Dn50_toe.
beta : float, optional, default: 0
angle between direction of wave approach and a line normal to
the breakwater (degrees).
layers : int, optional, default: 1
number of layers in the armour layer
layers_underlayer : int, optional, default: 2
number of layers in the underlayer
filter_rule : {'Rock', 'Xbloc', 'XblocPlus'}, optional, default: None
filter rule to use for the substructure of the breakwater, for
Rock, Xbloc and XblocPlus the correct filter rule is
automatically selected. In case another type of armour layer is
used one of these filter rules must be chosen.
Soil : :py:class:`Soil`, optional, default: None
by default Soil is None, which means that the geotechnical checks
are not performed. By specifying a Soil object, the geotechnical
checks are automatically performed.
phi : float, optional, default: 40
internal friction angle of rock [degrees]
id : int, optional, default: None
add a unique id to the breakwater
Attributes
----------
logger : dict
dict of warnings and messages
structure : dict
dictionary with the computed Dn50, rock class, and average Dn50
of the rock class for each layer and the toe. This dictionary
includes all variants, use :py:meth:`get_variant` to get the
parameters of one specific variant. Alternatively,
:py:meth:`print_variant` can be used to print the details of
one, multiple or all variants.
alpha : float
slope of the structure in radians
id : int
unique id of the breakwater
variantIDs : list
list with the IDs of the variants generated for this rubble
mound breakwater.
Rc : float
the crest freeboard of the structure [m]
width_scour : float
the required length of the scour protection [m]
"""
def __init__(
self, slope, slope_foreshore, B, rho_w, LimitState, ArmourUnit,
Grading, Dn50_core, safety=1, slope_toe=(2,3), B_toe=None, beta=0,
layers=1, layers_underlayer=2, filter_rule=None, Soil=None,
phi=40, id=None, **kwargs):
""" See help(ConcreteRubbleMound) for more info """
# set logger and structure
self.logger = {'INFO': [], 'WARNING': []}
self.structure = {}
# compute angles
self.alpha = np.arctan(slope[0]/slope[1])
slope_foreshore = np.arctan(slope_foreshore[0]/slope_foreshore[1])
# compute relative buoyant density
self.id = id
self.variantIDs = ['a', 'b', 'c', 'd']
# compute relative buoyant density
delta_armour = (ArmourUnit.rho - rho_w)/rho_w
# convert single LimitState to list if needed
if isinstance(LimitState, list):
LimitStates = LimitState
else:
LimitStates = [LimitState]
# determine the type of armour layer
armour_layer = ArmourUnit.name
if armour_layer in ['Xbloc', 'XblocPlus']:
# the formula for Xbloc and XblocPlus is based on the
# Hudson formula with a fixed slope of 3:4
angle = np.arctan(3/4)
filter_rule = armour_layer
else:
angle = self.alpha
# raise error if no filter rule is specified
if filter_rule is None:
supported_rules = ', '.join(substructure._supported_armour_layers())
raise NotSupportedError(
(f'Filter rule for {armour_layer} is not implemented, set '
f'filter rule to use with filter_rule to {supported_rules}'))
# set temporary values to check changes
dn, state = 0, 0
for i, LimitState in enumerate(LimitStates):
# unpack Hydraulic Conditions
Hs = LimitState.get_Hs(definition='H13')
# design armour layer
dn_temp = hudson(
H=Hs, Kd=ArmourUnit.kd, Delta=delta_armour, alpha=angle)
# check if computed Dn50 of current LimitState is larger
# than current normative Dn50
if dn_temp > dn:
# if larger the normative Dn50 must be changed
dn = dn_temp
state = i
V_required = ArmourUnit.get_class(dn)
self.structure['armour'] = {'computed Dn50': dn,
'class': V_required,
'class Dn50': V_required**(1/3),
'state': state,
'layers': layers}
# design underlayer, filter layer and crest height
super().__init__(
Dn50=V_required**(1/3), Dn50_core=Dn50_core,
rho=ArmourUnit.rho, rho_w=rho_w, armour_layer=armour_layer,
layers=layers, LimitStates=LimitStates, Grading=Grading,
safety=safety, layers_underlayer=layers_underlayer,
slope_toe=slope_toe, B_toe=B_toe, slope=slope, B=B, beta=beta,
id=id, filter_rule=filter_rule, Soil=Soil, phi=phi, **kwargs)
# check if a correction factor must be applied for Xbloc or
# XblocPlus (DMC design guidelines for Xbloc and XblocPlus)
correction = getattr(ArmourUnit, 'correction_factor', None)
if callable(correction):
# make dict to pass to method correction_factor
params = {
'Hs': LimitStates[state].get_Hs('H13'),
'h': LimitStates[state].h,
'Rc': self.Rc,
'occurrence_hs': False,
'slope': self.alpha,
'slope_foreshore': slope_foreshore,
'permeability': 'permeable',
'Dn': dn,
'layers': layers,
'B': B,
'beta': beta,
}
correction_factor = ArmourUnit.correction_factor(
logger=self.logger, **params)
else:
self.logger['INFO'].append(
'Given ArmourUnit did not have a method to compute a '
'correction factor. See documentation how to define a method '
'to compute a correction factor for a custom armour unit')
correction_factor = 1.
# check if a correction factor must be applied
if correction_factor != 1:
new_dn = correction_factor**(1/3) * dn
# replave values in structure with new ones for armour
new_class = ArmourUnit.get_class(new_dn)
self.structure['armour'] = {'computed Dn50': new_dn,
'class': new_class,
'class Dn50': new_class**(1/3),
'state': state,
'layers': layers}
# check if the class of the units has changed
if new_class != V_required:
self.logger['INFO'].append(
'correction factor resulted in new class, so design will '
'be changed')
# because of the new class the design for the underlayer
# and filter layer must be changed as well
super().__init__(
Dn50=new_class**(1/3), Dn50_core=Dn50_core,
rho=ArmourUnit.rho, rho_w=rho_w, armour_layer=armour_layer,
layers=layers, LimitStates=LimitStates, Grading=Grading,
safety=safety, layers_underlayer=layers_underlayer,
slope_toe=slope_toe, B_toe=B_toe, slope=slope, B=B,
beta=beta, id=id, filter_rule=filter_rule, Soil=Soil,
phi=phi, **kwargs)
else:
# same class so design does not have to be changed
self.logger['INFO'].append(
'correction factor did not result in a new class, so '
'design will not be changed')
def __str__(self):
return (f'id.{self.id}: breakwater with armour units as armour layer, '
f'and variants: {self.variantIDs}')
[docs] def cost(
self, *variants, core_price, unit_price, transport_cost=None,
output='variant'):
""" Compute the cost per meter for each variant
Method to compute the cost of each generated variant, the cost
is computed per meter. The cost of the rocks in the substructure
must be specified in the RockGrading. If transport cost are not
included in the price of rocks or core_price it can be given
with the argument transport_cost.
.. note::
The transport_cost are not added to the price of the armour
layer. The assumption has been made that the cost of
producing and transporting the armour units is included in
the unit_price.
Parameters
----------
*variants : str
IDs of the variants to plot, see :py:attr:`variantIDs` for
a list of all generated variants. If 'all' is in the
arguments, all variants will be plotted.
core_price : float
cost of the core material per m³
unit_price : float
the cost of an armour unit per m³
transport_cost : float, optional, default: None
the cost to transport a m³ of rock from the quarry to the
project location
output : {variant, layer, average}
format of the output dict, variant returns the total cost
of each variant, layer the cost of each layer for each
variant and average returns the average cost.
Returns
-------
dict
the cost
Raises
------
RockGradingError
if no pricing is included in the given RockGrading
"""
# compute the cost of the concept
cost = self._cost(
*variants, core_price=core_price, unit_price=unit_price,
transport_cost=transport_cost, output=output)
return cost