### ===================================================================================================================
### Definition of the step walking load
### ===================================================================================================================
# This calculation tool is based on the Dutch SBR guideline **Trillingen van vloeren door lopen**
# Copyright ©2026 Haskoning Nederland B.V.
### ===================================================================================================================
### 1. Import modules
### ===================================================================================================================
# General imports
from warnings import warn
import numpy as np
from pathlib import Path
from fpdf import (
FPDF,
XPos,
YPos
)
from typing import (
Optional,
Union,
List,
)
# References for functions and classes in the haskoning_atr_tools package
from haskoning_atr_tools.config import Config
from haskoning_atr_tools.human_induced_vibrations.config import ATRConfigHumanInducedVibrations as ATRConfig
from haskoning_atr_tools.human_induced_vibrations.step_load import StepLoad
# References for functions and classes in the haskoning_structural package
from haskoning_atr_tools import installed_modules
if 'haskoning_structural' in installed_modules:
# Only used in method to_fem, which uses the optional package for Haskoning DataFusr structural
from haskoning_structural.general import Timeseries
from haskoning_structural.fem_status import Project as FemProject
fem_use = True
else:
Timeseries = None
FemProject = None
fem_use = False
# Import third-party libraries
import matplotlib
matplotlib.use(Config.MPL_NONINTERACTIVE(notify=False))
import matplotlib.pyplot as plt
### ===================================================================================================================
### 2. StepWalkingLoad class
### ===================================================================================================================
[docs]
class StepWalkingLoad:
""" The StepWalkingLoad class represents the load generated by a series of walking steps."""
# Setting for the minimum number of steps in the step walking load
_minimum_nr_steps = 15
# Setting for the reduction of the load in the first and last (reversed) number of steps
_load_factor_start_end = [0.2, 0.4, 0.6, 0.8]
[docs]
def __init__(
self, step_load: StepLoad, nr_steps: int = 15, load_reduction: bool = False,
step_size: Optional[float] = None):
"""
Input:
- step_load (float): The definition for the single step in the walking load. Instance of StepLoad class.
- nr_steps (int): The number of steps to apply in the walking load. The total amount of steps should be at
least 15. Default value is 15.
- load_reduction (bool): Toggle to apply a load reduction is applied to the first four and last four
steps (load reduction 80%, 60%, 40% and 20% per step). Default value is False, no load reduction is
applied.
- step_size (float): Optional input to directly set the time resolution, in [s], for evaluating the walking
load. If None, a default value based on step frequency is used. Recommended to be small enough to capture
step dynamics accurately. Maximum allowed value is 1/50 of the interval between steps (factor set in the
config file).
"""
warn(
"WARNING:The Human Induced Vibrations Tool is currently under active development. Functionality may change "
"in future updates, and formal validation is still pending. Please verify your results carefully before "
"using them in your applications.")
self.step_load = step_load
self.nr_steps = nr_steps
self.load_reduction = load_reduction
self.step_size = step_size
def __repr__(self):
return f"ATR: {self.name}"
@property
def name(self) -> str:
name = f'StepWalkingLoad - {self.step_load.body_mass}kg, {self.step_load.step_frequency}Hz, #{self.nr_steps}'
if self.load_reduction:
return name + ', reduced'
return name
@property
def step_load(self):
return self.__step_load
@step_load.setter
def step_load(self, new_step_load: StepLoad):
if not isinstance(new_step_load, StepLoad):
raise TypeError(
f"ERROR: The step load for the single step should be provided as instance of {StepLoad.__name__} "
f"class. Provided was {type(new_step_load)}. Please correct your input.")
self.__step_load = new_step_load
@property
def nr_steps(self):
return self.__nr_steps
@nr_steps.setter
def nr_steps(self, new_nr_steps: int = 15):
if not isinstance(new_nr_steps, int):
raise TypeError(
f"ERROR: The number of steps for the step walking load should be provided as integer. Provided was "
f"{type(new_nr_steps)}. Please correct your input.")
if new_nr_steps < self._minimum_nr_steps:
raise ValueError(
f"ERROR: The value of the number of steps should be larger than 15. Provided was {new_nr_steps} steps.")
self.__nr_steps = new_nr_steps
@property
def load_reduction(self):
return self.__load_reduction
@load_reduction.setter
def load_reduction(self, new_load_reduction: bool = False):
if not isinstance(new_load_reduction, bool):
raise TypeError(
f"ERROR: The number of steps for the step walking load should be provided as integer. Provided was "
f"{type(new_load_reduction)}. Please correct your input.")
if self.method == 'SBR' and not new_load_reduction:
warn(
f"WARNING: According to the SBR load reduction should be applied, but this is not applied now. Please "
f"check if this is intended.")
self.__load_reduction = new_load_reduction
@property
def step_size(self) -> float:
return self.__step_size
@step_size.setter
def step_size(self, new_step_size: Optional[float] = None):
if new_step_size is None:
new_step_size = self.interval / ATRConfig.STEP_SIZE_FACTOR
elif new_step_size <= 0:
raise ValueError("ERROR: Input for step_size of StepWalkingLoad instance must be a positive float.")
elif new_step_size > ATRConfig.STEP_SIZE_FACTOR / self.step_load.step_frequency:
raise ValueError(
f"ERROR: Input for step_size of StepWalkingLoad instance is too large to capture step dynamics "
f"accurately. Maximum allowed value is {ATRConfig.STEP_SIZE_FACTOR / self.step_load.step_frequency:.4f}"
f" s, provided was {new_step_size}. Please correct your input.")
self.__step_size = new_step_size
@property
def method(self) -> str:
""" Returns the method used for the calculation, currently only SBR is available, as string."""
return 'SBR'
@property
def total_walking_time(self) -> float:
""" Duration of the complete walking load, in [s]."""
return ((self.nr_steps - 1) / self.step_load.step_frequency) + self.step_load.step_load_duration
@property
def interval(self) -> float:
""" Time between two consecutive steps, according SBR, in [s]."""
return 1 / self.step_load.step_frequency
@property
def time_domain(self) -> List[float]:
""" Returns list of values of the time steps for the step walking load, in [s]."""
time_domain = np.arange(0, self.total_walking_time, self.step_size).tolist()
# Ensuring last value of time_domain equal to total_walking_time
if time_domain[-1] != self.total_walking_time:
time_domain[-1] = self.total_walking_time
return time_domain
@property
def step_walking_loads(self) -> List[float]:
""" Returns list of values of the step walking load for a series of steps, in [N]."""
# The user can specify to apply load reduction for the first and last steps
if self.load_reduction:
# Define reduction factors for the beginning and the end
reductions = \
self._load_factor_start_end + [1.0] * (self.nr_steps - 2 * len(self._load_factor_start_end)) + \
self._load_factor_start_end[::-1]
else:
# No reductions applied
reductions = [1.0] * self.nr_steps
# Calculate the walking load
walking_load_array = np.zeros(len(self.time_domain)).tolist()
for j, t in enumerate(self.time_domain):
walking_load_array[j] = sum([
self.step_load.calculate_step_load_at_time(
defined_time=t - (i / self.step_load.step_frequency)) * reductions[i] for i in range(self.nr_steps)
if 0 < t - (i / self.step_load.step_frequency) < self.step_load.step_load_duration])
# Return the calculated walking load
return walking_load_array
[docs]
def to_fem(self, project: FemProject, name: Optional[str] = None) -> Timeseries:
"""
Method of StepWalkingLoad to convert the instance to a Timeseries object in the haskoning_structural library,
and added to the provided FEM project.
Input:
- project (obj): Object reference of the project in the haskoning_structural module.
- name (str): Optional input for the name of the time-series function. If not provided it will be numbered
based on the function ID in the FEM project.
Output:
- Returns the object created in the 'Timeseries' class of the haskoning_structural library.
- The timeseries is added as function to the FEM project collections.
"""
if not fem_use:
raise ImportError(
"ERROR: This functionality requires the haskoning_structural module to be installed. Please proceed "
"after installation.")
if name is None:
name = self.name
return project.create_timeseries_function(
time_series=self.time_domain, value_series=self.step_walking_loads, name=name)
[docs]
def plot(
self, show: bool = True, save_file: Optional[Union[str, Path]] = None, normalised: bool = False,
add_title: bool = True) -> Union[plt.Figure, Path]:
"""
Generate a plot of the step walking load over time.
Input:
- show (bool): Switch to indicate if the pop-up screen of the graph should appear. Default value is True.
- save_file (str or Path): The path of the image if the plot should be saved. If None the image will not be
saved. Default is None.
- normalised (bool): Select to create the plot for the normalised load over time. Default value is False.
- add_title (bool): Select to add a title to the plot. Default value is True.
Output:
- Returns the matplotlib figure when save_file is None.
- Returns the path to the created image if save_file has been provided.
"""
# Check if normalised plot is requested
load_values = self.step_walking_loads
if normalised:
load_values = [v / self.step_load.body_mass for v in load_values]
# Initialise matplotlib for viewing
if show:
matplotlib.use(Config.MPL_GUI())
# Create the plot
plt.close()
plt.style.use(Config.PLOT_STYLE_FILE.as_posix())
plt.plot(self.time_domain, load_values)
plt.xlabel('Time [s]')
if normalised:
plt.ylabel('Normalised load [-]')
if add_title:
plt.title('Normalised load vs time')
else:
plt.ylabel('Load [kg]')
if add_title:
plt.title('Load vs time')
plt.grid(True)
if save_file:
plt.savefig(fname=save_file, bbox_inches='tight', dpi=150)
# Show the plot if requested
if show:
plt.show()
# Set matplotlib backend back to non-interactive if changed to GUI backend
matplotlib.use(Config.MPL_NONINTERACTIVE())
# Return the file if there is a file generated
plt.close()
if save_file:
return save_file
return plt
[docs]
def to_pdf(self, file_path: Union[Path, str] = None, include_time_domain_loads: bool = False) -> Optional[Path]:
"""
Create a summary of the step walking load, including the input parameters and a plot of the load over time. The
summary is saved as a PDF file."""
def print_dict_items(data: dict, pdf_object: FPDF, units: dict, keys: List[str]):
for item in data.keys():
if item in keys:
_value = data[item]
if isinstance(_value, bool): # bool is an int in Python, so we need to check for bool before int
pass
elif isinstance(_value, (float, int)):
_value = f"{_value:.2f}"
elif isinstance(_value, list) and all(isinstance(_, (float, int)) for _ in _value):
_value = [round(_, 2) for _ in _value]
text = f"{item} = {_value}"
if item in units.keys():
text += f" {units[item]}"
pdf_object.multi_cell(
w=standard_width, h=normal_line_spacing, text=text, align='L')
pdf_object.ln(normal_line_spacing)
if file_path is None:
file_path = Path().cwd() / f'/{self.name}_summary.pdf'
plot_path = file_path.with_stem(file_path.stem.replace('_summary', '')).with_suffix('.png')
data = {
'name': self.name,
'body_mass': self.step_load.body_mass,
'step_frequency': self.step_load.step_frequency,
'nr_steps': self.nr_steps,
'load_reduction': self.load_reduction,
'step_size': self.step_size,
'method': self.method,
'total_walking_time': self.total_walking_time,
'interval': self.interval,
'time_domain': self.time_domain,
'step_walking_loads': self.step_walking_loads,
'plot': self.plot(show=False, normalised=False, save_file=plot_path),
}
units = {
'body_mass': 'kg',
'step_frequency': 'Hz',
'step_size': 's',
'total_walking_time': 's',
'interval': 's',
'time_domain': 's',
'step_walking_loads': 'N',
}
title_text_size = 12
normal_text_size = 8
normal_line_spacing = 4
standard_width = 190
table_width = 180
summary = FPDF(orientation='P', unit='mm', format='A4')
font = "helvetica"
font_style = ''
font_bold = "helvetica"
font_style_bold = 'B'
font_loc = Path(r"C:\Windows\Fonts\arial.ttf")
font_bold_loc = Path(r"C:\Windows\Fonts\arialbd.ttf")
if font_loc.exists() and font_bold_loc.exists():
font = "Arial_regular"
font_style = ''
summary.add_font(font, "", font_loc.as_posix())
font_bold = "Arial_bold"
font_style_bold = ''
summary.add_font(font_bold, "", font_bold_loc.as_posix())
summary.add_page()
summary.set_font(family=font_bold, style=font_style_bold, size=title_text_size)
summary.set_fill_color(r=220, g=220, b=220)
summary.cell(
w=standard_width, h=normal_text_size, text=data['name'], new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
summary.ln(normal_line_spacing)
summary.set_font(family=font, style=font_style, size=normal_text_size)
print_dict_items(
data=data, pdf_object=summary, units=units,
keys=['name', 'body_mass', 'step_frequency', 'nr_steps', 'load_reduction', 'step_size', 'method',
'total_walking_time', 'interval'])
x_y = [summary.get_x(), summary.get_y()].copy()
summary.image(x=x_y[0], y=x_y[1], name=data['plot'].as_posix(), w=table_width)
if include_time_domain_loads:
summary.add_page()
print_dict_items(
data=data, pdf_object=summary, units=units, keys=['time_domain', 'step_walking_loads'])
if file_path.exists():
print(
f"An existing step walking load summary is found. The existing summary ({file_path.as_posix()}) will be "
f"removed before the new one will be saved.")
file_path.unlink()
summary.output(file_path.as_posix())
if file_path.exists():
print(f"Step walking load summary is saved at {file_path.as_posix()}.")
return file_path
else:
print(f"WARNING: Step walking load summary is not saved.")
return None
### ===================================================================================================================
### 3. End of script
### ===================================================================================================================