Source code for haskoning_atr_tools.human_induced_vibrations.step_walking_load

### ===================================================================================================================
###  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 ### ===================================================================================================================