Skip to Content
DataCards 2.2.4 is released šŸŽ‰
DocumentationTutorialMaritime Data Product Tutorial

Maritime Data Product Tutorial

Introduction

This tutorial demonstrates how to build a comprehensive maritime data product using DataCards’ modular workflow approach. The system processes ship data, environmental conditions, and anchoring requirements to provide intelligent anchorage recommendations.

The data product consists of six interconnected modules, each handling specific aspects of maritime data processing:

  1. Schiff (Ship) - Central data hub and ship database
  2. Windkraftmodell (Wind Power Model) - Wind strength analysis and power calculations
  3. Wasserkraftmodell (Hydro Power Model) - Current strength, tides, and hydro power analysis
  4. Datenprodukt (Data Product) - Core processing with original vs reference path comparison
  5. Ankermodell (Anchor Model) - Anchor positioning and holding power calculations
  6. Wassertiefe (Water Depth) - Depth analysis with real-time gauge data integration

Each module publishes specific variables that connect notebooks, enabling a flexible, modular architecture where components can be developed and tested independently.

Architecture Overview

The maritime data product follows a data flow architecture where:

  • Input data flows from ship systems and environmental sources
  • Processing modules transform and analyze the data
  • Published variables enable notebook connections and data sharing
  • Feedback loops allow real-time adjustments based on current conditions

This modular approach ensures:

  • Scalability: Each module can be scaled independently
  • Maintainability: Clear separation of concerns
  • Testability: Individual modules can be tested in isolation
  • Flexibility: Easy to add new data sources or modify existing logic

Module 1: Schiffsdaten (Ship Data)

Purpose

Simple input module for ship data using DataCards slider cards. This module provides the basic ship information needed by other modules, matching the structure shown in the screenshot.

DataCards Input Cards

The Ship module contains three essential data elements as DataCards input cards:

  1. Positionsdaten Schiff (LONG, LAT) - Ship position data with longitude and latitude coordinates
  2. Stammdaten Schiff - Ship master data and core vessel information
  3. Peilung (K_rw) [grad oder strich] - Bearing data in degrees or strich (navigation units)

Positionsdaten (Position Data)

# Ship Position - Latitude # First publish the variable datacards.publish.variable('latitude_input', 55.0) # Then publish the card that consumes it datacards.publish.card( type='floatSlider', label='Latitude', unit='°N', min=50.0, max=60.0, step=0.01, variable_key='latitude_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (0,0), "deck": "default-deck"}] ) # Ship Position - Longitude # First publish the variable datacards.publish.variable('longitude_input', 10.0) # Then publish the card that consumes it datacards.publish.card( type='floatSlider', label='Longitude', unit='°E', min=5.0, max=15.0, step=0.01, variable_key='longitude_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (3,0), "deck": "default-deck"}] )

Stammdaten Schiff (Ship Master Data)

Vessel Name

# First publish the variable datacards.publish.variable('vessel_name_input', 'MV DataCard Explorer') # Then publish the card that consumes it datacards.publish.card( type='text', label='Vessel Name', variable_key='vessel_name_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (0,2), "deck": "default-deck"}] )

Gross Tonnage

# First publish the variable datacards.publish.variable('gross_tonnage_input', 5000) # Then publish the card that consumes it datacards.publish.card( type='intSlider', label='Gross Tonnage', unit='GT', min=100, max=10000, step=50, variable_key='gross_tonnage_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (3,2), "deck": "default-deck"}] )

Vessel Length

# First publish the variable datacards.publish.variable('vessel_length_input', 120.0) # Then publish the card that consumes it datacards.publish.card( type='floatSlider', label='Vessel Length', unit='m', min=50.0, max=200.0, step=1.0, variable_key='vessel_length_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (0,4), "deck": "default-deck"}] )

Vessel Width

# First publish the variable datacards.publish.variable('vessel_width_input', 18.0) # Then publish the card that consumes it datacards.publish.card( type='floatSlider', label='Vessel Width', unit='m', min=10.0, max=50.0, step=0.5, variable_key='vessel_width_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (3,4), "deck": "default-deck"}] )

Draft

# First publish the variable datacards.publish.variable('draft_input', 6.5) # Then publish the card that consumes it datacards.publish.card( type='floatSlider', label='Draft', unit='m', min=2.0, max=15.0, step=0.1, variable_key='draft_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (0,6), "deck": "default-deck"}] )

Peilung (Bearing)

Bearing

# First publish the variable datacards.publish.variable('bearing_input', 180.0) # Then publish the card that consumes it datacards.publish.card( type='floatSlider', label='Bearing', unit='°', min=0.0, max=360.0, step=1.0, variable_key='bearing_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (3,6), "deck": "default-deck"}] )

Bearing Units Toggle

# First publish the variable datacards.publish.variable('bearing_units_input', False) # Then publish the card that consumes it datacards.publish.card( type='toggle', label='Units (Degrees/Strich)', variable_key='bearing_units_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (6,6), "deck": "default-deck"}] )

Additional ship data needed for Windkraftmodell

Lateralplan (Lateral Plan Area)

# First publish the variable datacards.publish.variable('lateralplan_input', 50.0) # Then publish the card that consumes it datacards.publish.card( type='floatSlider', label='Lateral Plan Area', unit='m²', min=10.0, max=200.0, step=1.0, variable_key='lateralplan_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (0,8), "deck": "default-deck"}] )

Bootstyp (Boat Type)

# First publish the variable datacards.publish.variable('bootstyp_input', 'motor') # Then publish the card that consumes it datacards.publish.card( type='combobox', label='Boat Type', options=['segel', 'motor'], variable_key='bootstyp_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (3,8), "deck": "default-deck"}] )

Masthƶhe (Mast Height)

# First publish the variable datacards.publish.variable('masthoehe_input', 15.0) # Then publish the card that consumes it datacards.publish.card( type='floatSlider', label='Mast Height', unit='m', min=5.0, max=50.0, step=0.5, variable_key='masthoehe_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (6,8), "deck": "default-deck"}] )

Published Variables

Variable NameTypeDescriptionSource
latitude_inputfloatShip latitude positionSlider input
longitude_inputfloatShip longitude positionSlider input
vessel_name_inputstringVessel nameText input
gross_tonnage_inputintGross tonnageSlider input
vessel_length_inputfloatVessel length in metersSlider input
vessel_width_inputfloatVessel width in metersSlider input
draft_inputfloatVessel draft in metersSlider input
bearing_inputfloatBearing in degreesSlider input
bearing_units_inputboolUnits toggle (degrees/strich)Toggle input
lateralplan_inputfloatLateral plan area in m²Slider input
bootstyp_inputstringBoat type (segel/motor)Combobox input
masthoehe_inputfloatMast height in metersSlider input

Module 2: Windkraftmodell (Wind Power Model)

Purpose

Calculates wind force and wind attack on the ship based on wind speed, ship dimensions, and orientation. The model implements the proper maritime wind calculations including land cover corrections, height adjustments, and presented attack surface calculations.

DataCards Input Cards

Abdeckung (Coverage) Slider

# Abdeckung (Coverage) - Wind Coverage Factor # First publish the variable datacards.publish.variable('abdeckung_input', 1.0) # Then publish the card that consumes it datacards.publish.card( type='floatSlider', label='Wind Coverage Factor', unit='Factor', min=0.0, max=1.0, step=0.01, variable_key='abdeckung_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (0,0), "deck": "default-deck"}] )

Published Variables

Variable NameTypeDescriptionSource
abdeckung_inputfloatWind coverage factor (0.0-1.0)Slider input
windstaerkeSeriesWind strength (Beaufort scale or m/s)Meteorological data, ship sensors
windkraftSeriesTotal wind force on the ship (kN)Calculated from wind strength, ship profile
windengriffSeriesWind attack area / effective windage area (m²)Ship master data, design parameters

Implementation

# Wind Power Model Notebook import pandas as pd import numpy as np # Consume ship data from Schiffsdaten module lateralplan = dc.consume.variable.lateralplan_input() bootstyp = dc.consume.variable.bootstyp_input() masthoehe = dc.consume.variable.masthoehe_input() # Consume wind coverage factor abdeckung_factor = dc.consume.variable.abdeckung_input() # Create placeholder data (since these variables don't exist yet) # In a real implementation, these would be consumed from other modules wind_data = pd.DataFrame({ 'wind_speed_10m': np.random.uniform(0, 25, 100), 'wind_direction_10m': np.random.uniform(0, 360, 100), 'wind_speed_50m': np.random.uniform(0, 30, 100), 'wind_direction_50m': np.random.uniform(0, 360, 100), 'gust_speed': np.random.uniform(0, 35, 100) }) ship_data = pd.DataFrame({ 'latitude': np.random.uniform(53.5, 54.5, 100), 'longitude': np.random.uniform(10.0, 11.0, 100), 'heading': np.random.uniform(0, 360, 100), 'speed': np.random.uniform(0, 20, 100) }) stammdaten = pd.DataFrame([{ 'vessel_name': 'MV DataCard Explorer', 'beam': 18.0, # BÜA 'length_overall': 120.0, # LÜA 'gross_tonnage': 5000, 'draft_max': 6.5 }]) def apply_abdeckung_correction(wind_speed, abdeckung_factor): """ Apply coverage correction to wind speed based on land shelter effects. Based on the technical diagrams: - 1.0 = No coverage (open sea) - 0.7-0.9 = Steep coast coverage - 0.5-0.8 = Tree coverage """ corrected_wind_speed = wind_speed * abdeckung_factor return corrected_wind_speed def apply_height_correction(wind_speed, reference_height=12, target_height=3): """ Apply height correction using Power Law (Potenzgesetz). Formula: v(z) = v(z_ref) * (z / z_ref)^α Where α = 0.11 for open sea (roughness factor) """ alpha = 0.11 # Roughness factor for open sea if reference_height <= 0 or target_height <= 0: return wind_speed height_ratio = target_height / reference_height corrected_wind_speed = wind_speed * (height_ratio ** alpha) return corrected_wind_speed def calculate_wind_pressure(wind_speed): """ Calculate wind pressure using the formula: p = ½ * ρ * v² For standard conditions (sea level, 15°C): ρ = 1.225 kg/m³ p = 0.6125 * v² (with v in m/s, p in N/m²) """ # Simplified formula for standard conditions wind_pressure = 0.6125 * (wind_speed ** 2) return wind_pressure def calculate_presented_width(boat_beam, boat_length, wind_angle_deg): """ Calculate presented width using trigonometric function. Formula: W[φ] = | B * cos(φ) + L * sin(φ) | Where: - B = BÜA (Breite über Alles - Overall Beam) - L = LÜA (LƤnge über Alles - Overall Length) - φ = angle between bow direction and wind direction """ # Convert angle to radians wind_angle_rad = np.radians(wind_angle_deg) # Calculate presented width presented_width = abs( boat_beam * np.cos(wind_angle_rad) + boat_length * np.sin(wind_angle_rad) ) return presented_width def calculate_effective_height(cabin_height, mast_height): """ Calculate effective height for wind attack. Formula: H = cabin_height + 0.4 * mast_height This is an empirical value for wind attack on rigging. """ effective_height = cabin_height + 0.4 * mast_height return effective_height def calculate_wind_force(wind_pressure, presented_width, effective_height): """ Calculate total wind force on the ship. Formula: Windkraft = Winddruck * AngriffsflƤche Where: AngriffsflƤche = W * H (presented width * effective height) """ attack_surface = presented_width * effective_height wind_force = wind_pressure * attack_surface return wind_force def calculate_wind_angle(ship_heading, wind_direction): """ Calculate angle between ship bow direction and wind direction. φ = 0° means wind from front φ = 90° means wind from side """ # Calculate relative angle angle_diff = wind_direction - ship_heading # Normalize to 0-360 degrees angle_diff = angle_diff % 360 # Convert to -180 to +180 range if angle_diff > 180: angle_diff -= 360 return abs(angle_diff) # Return absolute value # Process wind data through the complete model windstaerke, windkraft, windengriff = process_wind_data( wind_data, ship_data, stammdaten, lateralplan, bootstyp, masthoehe, abdeckung_factor ) # Publish results dc.publish.variable('windstaerke', windstaerke) dc.publish.variable('windkraft', windkraft) dc.publish.variable('windengriff', windengriff)

Module 3: Wasserkraftmodell (Hydro Power Model)

Purpose

Calculates water current forces and hydro power effects on ship operations using proper maritime fluid dynamics. The model implements current speed analysis, dynamic pressure calculations, and underwater attack surface calculations for accurate hydro power modeling.

DataCards Input Cards

Strƶmung (Current) Slider

# Strƶmung (Current) - Current Speed # First publish the variable dc.publish.variable('stroemung_input', 2.0) # Then publish the card that consumes it dc.publish.card( type='floatSlider', label='Current Speed', unit='m/s', min=0.0, max=5.0, step=0.1, variable_key='stroemung_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (0,0), "deck": "default-deck"}] )

Gezeiten (Tidal) Sliders

Tidal Height

# First publish the variable dc.publish.variable('gezeiten_height_input', 1.2) # Then publish the card that consumes it dc.publish.card( type='floatSlider', label='Tidal Height', unit='m', min=0.0, max=3.0, step=0.1, variable_key='gezeiten_height_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (3,0), "deck": "default-deck"}] )

Tidal Velocity

# First publish the variable dc.publish.variable('gezeiten_velocity_input', 0.5) # Then publish the card that consumes it dc.publish.card( type='floatSlider', label='Tidal Velocity', unit='m/s', min=0.0, max=2.0, step=0.05, variable_key='gezeiten_velocity_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (6,0), "deck": "default-deck"}] )

Published Variables

Variable NameTypeDescriptionSource
stroemung_inputfloatCurrent speed (m/s)Slider input
gezeiten_height_inputfloatTidal height (m)Slider input
gezeiten_velocity_inputfloatTidal velocity (m/s)Slider input
stromstaerkeSeriesCurrent speed and direction (m/s, degrees)Current data from Ship module
gezeitenSeriesTidal height and velocity (m, m/s)Slider inputs
wasserkraftSeriesCalculated hydro power force (N)Dynamic pressure and attack surface calculations
stroemungsangriffSeriesCurrent attack force on ship (N)Final current attack force

Implementation

# Hydro Power Model Notebook import pandas as pd import numpy as np import datacards as dc # Hydro calculation functions def calculate_dynamic_pressure(current_speed, water_density=1005): """ Calculate dynamic pressure using the formula: q = ½ * ρ * v² Args: current_speed: Current speed in m/s water_density: Water density in kg/m³ (default: 1005 for Baltic Sea) Returns: Dynamic pressure in Pa """ dynamic_pressure = 0.5 * water_density * (current_speed ** 2) return dynamic_pressure def calculate_underwater_attack_surface(boat_beam, boat_length, current_angle_deg, draft): """ Calculate underwater attack surface using presented width formula. Based on maritime engineering principles: - Presented width = beam * sin(current_angle) + length * cos(current_angle) - Underwater area = presented width * draft Args: boat_beam: Ship beam in meters (BÜA) boat_length: Ship length in meters (LÜA) current_angle_deg: Angle between current and ship heading in degrees draft: Ship draft in meters Returns: Underwater attack surface in m² """ current_angle_rad = np.radians(current_angle_deg) # Calculate presented width presented_width = boat_beam * np.sin(current_angle_rad) + boat_length * np.cos(current_angle_rad) # Calculate underwater attack surface underwater_attack_surface = presented_width * draft return underwater_attack_surface def calculate_current_force(dynamic_pressure, attack_surface): """ Calculate current force on ship. Args: dynamic_pressure: Dynamic pressure in Pa attack_surface: Underwater attack surface in m² Returns: Current force in N """ current_force = dynamic_pressure * attack_surface return current_force def calculate_current_angle(ship_heading, current_direction): """ Calculate angle between ship bow direction and current direction. Args: ship_heading: Ship heading in degrees (0-360) current_direction: Current direction in degrees (0-360) Returns: Angle difference in degrees (0-180) """ angle_diff = abs(ship_heading - current_direction) # Normalize to 0-180 degrees if angle_diff > 180: angle_diff -= 360 return abs(angle_diff) # Return absolute value def get_baltic_sea_density(): """Get water density for Baltic Sea conditions""" return 1005 # kg/m³ def calculate_tidal_effects(tidal_data, timestamp, current_data_index=0): """Calculate tidal height and velocity effects""" if len(tidal_data) > 0 and 'tidal_component' in tidal_data.columns: # Use actual tidal data if available tidal_height = tidal_data['tidal_component'].iloc[current_data_index % len(tidal_data)] tidal_velocity = tidal_height * 0.1 # Simplified relationship else: # Fallback: simple sinusoidal tidal model hours_since_midnight = timestamp.hour + timestamp.minute / 60.0 tidal_height = 2.0 * np.sin(2 * np.pi * hours_since_midnight / 12.4) # 12.4 hour tidal cycle tidal_velocity = 2.0 * (2 * np.pi / 12.4) * np.cos(2 * np.pi * hours_since_midnight / 12.4) return tidal_height, tidal_velocity def process_hydro_data(ship_data, stammdaten, lateralplan, bootstyp, masthoehe, peilung, stroemung_speed): """Process hydro data through the complete hydro power model""" # Get ship dimensions from master data boat_beam = stammdaten['beam'].iloc[0] # BÜA boat_length = stammdaten['length_overall'].iloc[0] # LÜA draft = stammdaten['draft_max'].iloc[0] # Initialize results current_speeds = [] dynamic_pressures = [] hydro_forces = [] current_attacks = [] for i in range(len(ship_data)): # Get current ship data current_ship_heading = ship_data['heading'].iloc[i] # Use the consumed current speed from slider current_speed = stroemung_speed # Calculate current direction (simplified - could be from bearing) current_direction = peilung + np.random.uniform(-30, 30) # Add some variation # Step 1: Calculate dynamic pressure dynamic_pressure = calculate_dynamic_pressure(current_speed) # Step 2: Calculate current angle current_angle = calculate_current_angle(current_ship_heading, current_direction) # Step 3: Calculate underwater attack surface attack_surface = calculate_underwater_attack_surface(boat_beam, boat_length, current_angle, draft) # Step 4: Calculate current force current_force = calculate_current_force(dynamic_pressure, attack_surface) # Current attack is the same as current force in this model current_attack = current_force # Store results current_speeds.append(current_speed) dynamic_pressures.append(dynamic_pressure) hydro_forces.append(current_force) current_attacks.append(current_attack) return current_speeds, hydro_forces, current_attacks # Create Strömung (Current) slider input in this notebook dc.publish.variable('stroemung_input', 2.0) dc.publish.card( type='floatSlider', label='Current Speed', unit='m/s', min=0.0, max=5.0, step=0.1, variable_key='stroemung_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (0,0), "deck": "default-deck"}] ) # Create Gezeiten (Tidal) sliders # Tidal Height dc.publish.variable('gezeiten_height_input', 1.2) dc.publish.card( type='floatSlider', label='Tidal Height', unit='m', min=0.0, max=3.0, step=0.1, variable_key='gezeiten_height_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (3,0), "deck": "default-deck"}] ) # Tidal Velocity dc.publish.variable('gezeiten_velocity_input', 0.5) dc.publish.card( type='floatSlider', label='Tidal Velocity', unit='m/s', min=0.0, max=2.0, step=0.05, variable_key='gezeiten_velocity_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (6,0), "deck": "default-deck"}] ) # Consume ship data from Schiffsdaten module (as shown in diagram) lateralplan = dc.consume.variable.lateralplan_input() bootstyp = dc.consume.variable.bootstyp_input() masthoehe = dc.consume.variable.masthoehe_input() peilung = dc.consume.variable.bearing_input() # Consume ship master data from Schiffsdaten module vessel_name = dc.consume.variable.vessel_name_input() vessel_length = dc.consume.variable.vessel_length_input() vessel_width = dc.consume.variable.vessel_width_input() gross_tonnage = dc.consume.variable.gross_tonnage_input() draft = dc.consume.variable.draft_input() # Consume ship position data from Schiffsdaten module latitude = dc.consume.variable.latitude_input() longitude = dc.consume.variable.longitude_input() # Consume current speed from slider stroemung_speed = dc.consume.variable.stroemung_input() # Consume tidal data from sliders gezeiten_height = dc.consume.variable.gezeiten_height_input() gezeiten_velocity = dc.consume.variable.gezeiten_velocity_input() # Create ship master data from consumed variables stammdaten = pd.DataFrame([{ 'vessel_name': vessel_name, 'beam': vessel_width, # BÜA 'length_overall': vessel_length, # LÜA 'gross_tonnage': gross_tonnage, 'draft_max': draft }]) # Create ship position data from consumed variables ship_data = pd.DataFrame({ 'latitude': [latitude] * 100, # Use consumed latitude 'longitude': [longitude] * 100, # Use consumed longitude 'heading': np.random.uniform(0, 360, 100), # Random heading for now 'speed': np.random.uniform(0, 20, 100) # Random speed for now }) # Process hydro data through the complete model stromstaerke, wasserkraft, stroemungsangriff = process_hydro_data( ship_data, stammdaten, lateralplan, bootstyp, masthoehe, peilung, stroemung_speed ) # Create structured data for publishing stromstaerke_data = { 'speed': [float(x) for x in stromstaerke], 'direction': [float(x) for x in [peilung] * len(stromstaerke)] # Use bearing for direction } gezeiten = { 'height': [gezeiten_height] * len(stromstaerke), # Use slider value 'velocity': [gezeiten_velocity] * len(stromstaerke) # Use slider value } # Publish results dc.publish.variable('stromstaerke', stromstaerke) dc.publish.variable('gezeiten', gezeiten) dc.publish.variable('wasserkraft', wasserkraft) dc.publish.variable('stroemungsangriff', stroemungsangriff)

Module 4: Wassertiefe (Water Depth) - Real-time Depth Analysis

Purpose

Provides real-time water depth analysis using nautical charts, depth models, and current gauge data with feedback loops for dynamic adjustments. Consumes ship position data to determine water depth at the ship’s location.

DataCards Input Cards

Kartentiefe (Map Depth) Slider

# Kartentiefe (Map Depth) - Chart Depth # First publish the variable dc.publish.variable('kartentiefe_input', 15.0) # Then publish the card that consumes it dc.publish.card( type='floatSlider', label='Map Depth', unit='m', min=5.0, max=50.0, step=0.5, variable_key='kartentiefe_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (0,0), "deck": "default-deck"}] )

Published Variables

Variable NameTypeDescriptionSource
kartentiefe_inputfloatMap depth (m)Slider input
wassertiefe_berechnetfloatCalculated water depthDepth model calculations
tiefenwarnungdictDepth warnings and alertsDepth analysis
depth_analysisdictComplete depth analysis resultsWave and depth calculations
significant_wave_heightfloatSignificant wave height H_sWave data analysis

Implementation

# Water Depth Analysis Notebook import pandas as pd import numpy as np import datacards as dc # Consume ship position data from Schiffsdaten module latitude = dc.consume.variable.latitude_input() longitude = dc.consume.variable.longitude_input() # Consume map depth from slider kartentiefe = dc.consume.variable.kartentiefe_input() # Water depth calculation functions based on technical diagrams def calculate_significant_wave_height(wave_data): """ Calculate significant wave height H_s using spectral method. Formula: H_s = 4 * sqrt(m_0) Where m_0 is the zeroth moment of the spectrum = variance of sea level """ if len(wave_data) == 0: return 2.0 # Default value for Baltic Sea # Calculate variance of sea level (m_0) m_0 = np.var(wave_data) # Calculate significant wave height H_s = 4 * np.sqrt(m_0) return H_s def calculate_rayleigh_scale_parameter(H_s): """ Calculate Rayleigh scale parameter for wave heights. Formula: σ_Ray = H_s / 2 """ return H_s / 2 def calculate_wave_height_percentile(H_s, percentile=0.95): """ Calculate p-percentile of wave height using Rayleigh distribution. Formula: h_p = H_s * sqrt(-ln(1 - p) / 2) """ if percentile >= 1.0: percentile = percentile / 100.0 # Convert percentage to decimal h_p = H_s * np.sqrt(-np.log(1 - percentile) / 2) return h_p def calculate_maximum_water_column_depth(h_karte, H_s): """ Calculate highest possible water column depth. Formula: h_max = h_Karte + ½ H_s This gives the greatest depth in the wave trough. """ h_max = h_karte + 0.5 * H_s return h_max def calculate_effective_water_depth(h_karte, H_s, percentile=0.95): """ Calculate effective water depth considering wave effects. Combines chart depth with wave height percentiles. """ # Calculate maximum water column depth h_max = calculate_maximum_water_column_depth(h_karte, H_s) # Calculate wave height percentile h_p = calculate_wave_height_percentile(H_s, percentile) # Effective depth is the minimum of max depth and chart depth + wave effect effective_depth = min(h_max, h_karte + h_p) return effective_depth # Create wave data for the ship's position (simulated) # In a real implementation, this would come from Copernicus or other wave services def get_wave_data_for_position(lat, lon): """Get wave data for specific position (simulated)""" # Simulate wave data based on position # Baltic Sea typically has H_s = 0.5-3.0m base_wave_height = 1.5 + 0.5 * np.sin(lat * np.pi / 180) * np.cos(lon * np.pi / 180) # Generate time series of wave heights time_points = 100 wave_heights = np.random.normal(base_wave_height, 0.3, time_points) wave_heights = np.maximum(wave_heights, 0.1) # Ensure positive values return wave_heights # Process water depth analysis for ship's current position print(f"Analyzing water depth at position: {latitude:.4f}°N, {longitude:.4f}°E") # Get wave data for ship's position wave_data = get_wave_data_for_position(latitude, longitude) # Calculate significant wave height H_s = calculate_significant_wave_height(wave_data) print(f"Significant wave height (H_s): {H_s:.2f} m") # Calculate effective water depth effective_depth = calculate_effective_water_depth(kartentiefe, H_s) print(f"Chart depth: {kartentiefe:.2f} m") print(f"Effective water depth: {effective_depth:.2f} m") # Calculate maximum water column depth h_max = calculate_maximum_water_column_depth(kartentiefe, H_s) print(f"Maximum water column depth: {h_max:.2f} m") # Calculate wave height percentiles h_95 = calculate_wave_height_percentile(H_s, 0.95) h_99 = calculate_wave_height_percentile(H_s, 0.99) print(f"95th percentile wave height: {h_95:.2f} m") print(f"99th percentile wave height: {h_99:.2f} m") # Create depth analysis results depth_analysis = { 'position_lat': latitude, 'position_lon': longitude, 'chart_depth': kartentiefe, 'significant_wave_height': H_s, 'effective_depth': effective_depth, 'max_water_column_depth': h_max, 'wave_height_95th_percentile': h_95, 'wave_height_99th_percentile': h_99, 'analysis_timestamp': pd.Timestamp.now().isoformat() } # Create depth warning based on effective depth if effective_depth < 5.0: warning_level = "CRITICAL" warning_message = "Very shallow water - high grounding risk" elif effective_depth < 10.0: warning_level = "WARNING" warning_message = "Shallow water - proceed with caution" elif effective_depth < 20.0: warning_level = "CAUTION" warning_message = "Moderate depth - monitor closely" else: warning_level = "SAFE" warning_message = "Adequate water depth" tiefenwarnung = { 'level': warning_level, 'message': warning_message, 'effective_depth': effective_depth, 'chart_depth': kartentiefe, 'wave_effect': effective_depth - kartentiefe } # Publish results dc.publish.variable('wassertiefe_berechnet', effective_depth) dc.publish.variable('tiefenwarnung', tiefenwarnung) dc.publish.variable('depth_analysis', depth_analysis) dc.publish.variable('significant_wave_height', H_s)

Module 5: Ankermodell (Anchor Model)

Purpose

Calculates anchor positioning, holding power, and anchor requirements based on environmental conditions and ship characteristics.

Published Variables

Variable NameTypeDescriptionSource
ankergewichtfloatRequired anchor weight (kg)Environmental force calculations
ankertypstringRecommended anchor typeSeabed and depth analysis
bodengrundstringSeabed type classificationEnvironmental data
wetterdatenDataFrameWeather conditionsEnvironmental modules
ankerpositiondictRecommended anchor positionPosition analysis

DataCards Input Cards

Bodenprofil (Seabed Profile) Slider

# Bodenprofil (Seabed Profile) - Seabed Type # First publish the variable dc.publish.variable('bodenprofil_input', 'sand') # Then publish the card that consumes it dc.publish.card( type='combobox', label='Seabed Profile', options=['sand', 'schlamm', 'kiesel', 'kies', 'ton', 'lehm'], variable_key='bodenprofil_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (0,0), "deck": "default-deck"}] )

KettenlƤnge (Chain Length) Slider

# KettenlƤnge (Chain Length) - Chain Length # First publish the variable dc.publish.variable('kettenlaenge_input', 50.0) # Then publish the card that consumes it dc.publish.card( type='floatSlider', label='Chain Length', unit='m', min=10.0, max=200.0, step=5.0, variable_key='kettenlaenge_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (3,0), "deck": "default-deck"}] )

Implementation

# Anchor Model Notebook import pandas as pd import numpy as np import datacards as dc # Consume ship master data from Schiffsdaten module vessel_length = dc.consume.variable.vessel_length_input() vessel_width = dc.consume.variable.vessel_width_input() gross_tonnage = dc.consume.variable.gross_tonnage_input() draft = dc.consume.variable.draft_input() # Consume water depth from Wassertiefe module wassertiefe = dc.consume.variable.wassertiefe_berechnet() # Consume environmental forces from Wind and Hydro modules windkraft = dc.consume.variable.windkraft() wasserkraft = dc.consume.variable.wasserkraft() # Consume input sliders bodenprofil = dc.consume.variable.bodenprofil_input() kettenlaenge = dc.consume.variable.kettenlaenge_input() # Anchor efficiency data based on technical diagrams def get_anchor_efficiency_data(): """Get anchor efficiency (N/kg) by anchor type and soil type""" return { 'Danforth (Fluke)': {'sand': 150, 'schlamm': 150, 'kiesel': 100, 'kies': 100, 'ton': 200, 'lehm': 200}, 'CQR (Plow)': {'sand': 120, 'schlamm': 100, 'kiesel': 80, 'kies': 80, 'ton': 150, 'lehm': 150}, 'Delta (Plow)': {'sand': 130, 'schlamm': 110, 'kiesel': 90, 'kies': 90, 'ton': 160, 'lehm': 160}, 'Bruce (Claw)': {'sand': 100, 'schlamm': 80, 'kiesel': 70, 'kies': 70, 'ton': 120, 'lehm': 120}, 'Rocna (Scoop)': {'sand': 140, 'schlamm': 120, 'kiesel': 100, 'kies': 100, 'ton': 180, 'lehm': 180}, 'Spade (Scoop)': {'sand': 135, 'schlamm': 115, 'kiesel': 95, 'kies': 95, 'ton': 175, 'lehm': 175}, 'Admiralty/Stock': {'sand': 130, 'schlamm': 130, 'kiesel': 60, 'kies': 60, 'ton': 175, 'lehm': 175}, 'Mushroom': {'sand': 50, 'schlamm': 200, 'kiesel': 30, 'kies': 30, 'ton': 180, 'lehm': 180}, 'Grapnel': {'sand': 80, 'schlamm': 60, 'kiesel': 70, 'kies': 70, 'ton': 100, 'lehm': 100}, 'Kedge': {'sand': 90, 'schlamm': 70, 'kiesel': 80, 'kies': 80, 'ton': 110, 'lehm': 110} } def calculate_horizontal_force(windkraft, wasserkraft): """Calculate total horizontal environmental force Fy""" # Convert to numpy arrays if needed if hasattr(windkraft, 'values'): windkraft = windkraft.values if hasattr(wasserkraft, 'values'): wasserkraft = wasserkraft.values # Ensure same length min_len = min(len(windkraft), len(wasserkraft)) windkraft = windkraft[:min_len] wasserkraft = wasserkraft[:min_len] # Total horizontal force Fy = windkraft + wasserkraft return Fy def calculate_anchor_weight_requirement(Fy, vessel_length, gross_tonnage): """Calculate required anchor weight based on vessel size and environmental forces""" # Base weight calculation: 1 kg per 1000 kg displacement base_weight = gross_tonnage * 0.001 # Environmental factor: increase weight for higher forces max_force = np.max(Fy) if len(Fy) > 0 else 1000 force_factor = min(2.0, 1 + (max_force / 2000)) # Cap at 2x # Size factor: larger vessels need proportionally more anchor weight size_factor = 1 + (vessel_length / 100) # Increase with length required_weight = base_weight * force_factor * size_factor return max(required_weight, 10) # Minimum 10 kg def calculate_anchor_holding_power(anchor_weight, anchor_type, bodenprofil): """Calculate anchor holding power based on weight, type, and soil""" efficiency_data = get_anchor_efficiency_data() if anchor_type not in efficiency_data: anchor_type = 'Danforth (Fluke)' # Default if bodenprofil not in efficiency_data[anchor_type]: bodenprofil = 'sand' # Default efficiency = efficiency_data[anchor_type][bodenprofil] # N/kg holding_power = efficiency * anchor_weight # N return holding_power def calculate_chain_requirements(Fy, wassertiefe, kettenlaenge, bodenprofil): """Calculate chain requirements using catenary approach""" # Chain weight per meter (N/m) - typical values chain_weights = { 'sand': 25, 'schlamm': 25, 'kiesel': 30, 'kies': 30, 'ton': 20, 'lehm': 20 } w = chain_weights.get(bodenprofil, 25) # Default 25 N/m # Ground friction coefficient friction_coeffs = { 'sand': 0.6, 'schlamm': 0.4, 'kiesel': 0.7, 'kies': 0.7, 'ton': 0.5, 'lehm': 0.5 } mu = friction_coeffs.get(bodenprofil, 0.6) # Calculate for maximum force Fy_max = np.max(Fy) if len(Fy) > 0 else 1000 # Catenary calculations H = Fy_max # Horizontal force D = wassertiefe # Water depth L = kettenlaenge # Chain length # Parameter a = H/w a = H / w # Check if L >= 5*D (standard condition) if L < 5 * D: return { 'status': 'WARNING', 'message': f'Chain length {L}m is less than 5x depth {D}m. Minimum recommended: {5*D:.1f}m', 'effective_length': L, 'required_length': 5 * D } # Calculate effective chain length using catenary # cosh(x/a) = 1 + D/a + x/a # This is solved iteratively x = D * a # Initial guess for _ in range(10): # Iterative solution x_new = a * np.arccosh(1 + D/a + x/a) if abs(x_new - x) < 0.001: break x = x_new L_eff = a * np.sinh(x/a) # Effective chain length V = w * np.sinh(x/a) # Vertical component at anchor T_anker = np.sqrt(H**2 + V**2) # Total tension at anchor # Calculate friction from resting chain if L > L_eff: F_reibung = mu * w * (L - L_eff) T_anker_effective = T_anker - F_reibung # Reduced tension due to friction else: F_reibung = 0 T_anker_effective = T_anker return { 'status': 'OK', 'effective_length': L_eff, 'total_tension': T_anker, 'effective_tension': T_anker_effective, 'friction_force': F_reibung, 'vertical_component': V, 'horizontal_component': H } def determine_optimal_anchor_type(bodenprofil, vessel_length): """Determine optimal anchor type based on soil and vessel size""" # Anchor type recommendations by soil soil_recommendations = { 'sand': 'Danforth (Fluke)', 'schlamm': 'Mushroom', 'kiesel': 'Bruce (Claw)', 'kies': 'Bruce (Claw)', 'ton': 'Rocna (Scoop)', 'lehm': 'Rocna (Scoop)' } # For very large vessels, prefer more robust types if vessel_length > 80: if bodenprofil in ['sand', 'schlamm']: return 'Rocna (Scoop)' elif bodenprofil in ['kiesel', 'kies']: return 'Delta (Plow)' return soil_recommendations.get(bodenprofil, 'Danforth (Fluke)') # Main anchor model calculations print(f"=== ANCHOR MODEL CALCULATIONS ===") print(f"Vessel: {vessel_length}m LOA, {gross_tonnage} GT") print(f"Water depth: {wassertiefe:.2f} m") print(f"Seabed profile: {bodenprofil}") print(f"Chain length: {kettenlaenge:.1f} m") # Calculate horizontal environmental force Fy = calculate_horizontal_force(windkraft, wasserkraft) print(f"Maximum horizontal force: {np.max(Fy):.0f} N") # Calculate required anchor weight ankergewicht = calculate_anchor_weight_requirement(Fy, vessel_length, gross_tonnage) print(f"Required anchor weight: {ankergewicht:.1f} kg") # Determine optimal anchor type ankertyp = determine_optimal_anchor_type(bodenprofil, vessel_length) print(f"Recommended anchor type: {ankertyp}") # Calculate anchor holding power holding_power = calculate_anchor_holding_power(ankergewicht, ankertyp, bodenprofil) print(f"Anchor holding power: {holding_power:.0f} N") # Calculate chain requirements chain_analysis = calculate_chain_requirements(Fy, wassertiefe, kettenlaenge, bodenprofil) print(f"Chain analysis: {chain_analysis['status']}") # Calculate safety factor safety_factor = holding_power / np.max(Fy) if np.max(Fy) > 0 else float('inf') print(f"Safety factor: {safety_factor:.2f}") # Determine safety status if safety_factor >= 3.0: safety_status = "SAFE" safety_message = "Adequate holding power" elif safety_factor >= 2.0: safety_status = "CAUTION" safety_message = "Marginal holding power - monitor conditions" else: safety_status = "DANGER" safety_message = "Insufficient holding power - consider relocating" # Create anchor position recommendation ankerposition = { 'latitude': 54.0, # Would come from ship position 'longitude': 10.0, # Would come from ship position 'water_depth': wassertiefe, 'seabed_type': bodenprofil, 'safety_factor': safety_factor, 'safety_status': safety_status, 'safety_message': safety_message } # Create weather data summary wetterdaten = { 'max_wind_force': float(np.max(windkraft)) if len(windkraft) > 0 else 0, 'max_water_force': float(np.max(wasserkraft)) if len(wasserkraft) > 0 else 0, 'total_environmental_force': float(np.max(Fy)) if len(Fy) > 0 else 0, 'analysis_timestamp': pd.Timestamp.now().isoformat() } # Publish results dc.publish.variable('ankergewicht', ankergewicht) dc.publish.variable('ankertyp', ankertyp) dc.publish.variable('bodengrund', bodenprofil) dc.publish.variable('wetterdaten', wetterdaten) dc.publish.variable('ankerposition', ankerposition) print(f"\n=== ANCHOR MODEL RESULTS ===") print(f"Anchor Weight: {ankergewicht:.1f} kg") print(f"Anchor Type: {ankertyp}") print(f"Seabed: {bodenprofil}") print(f"Safety Status: {safety_status}") print(f"Safety Factor: {safety_factor:.2f}") print(f"Chain Status: {chain_analysis['status']}")

Module 6: Datenprodukt (Data Product) - Anchor Situation Analysis

Purpose

Creates the final data product that combines environmental forces (Gegenstress) with anchor resources (Ankerressource) to produce a comprehensive table and text showing how to correctly anchor in the current situation.

Published Variables

Variable NameTypeDescriptionSource
gegenstressDictEnvironmental stress forcesWind + Hydro modules
ankerressourceDictAnchor capacity and resourcesAnchor model
bruchlastFloatBreaking load calculationStress vs Resource analysis
anker_situation_tableDataFrameComplete anchoring situation tableData product analysis
anker_empfehlungStringText recommendation for anchoringSituation analysis

DataCards Input Cards

Safety Factor Slider

# Safety Factor - Additional safety margin # First publish the variable dc.publish.variable('safety_factor_input', 3.0) # Then publish the card that consumes it dc.publish.card( type='floatSlider', label='Safety Factor', unit='', min=1.5, max=5.0, step=0.1, variable_key='safety_factor_input', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (0,0), "deck": "default-deck"}] )

Implementation

# Data Product Notebook - Final Anchor Situation Analysis import pandas as pd import numpy as np import datacards as dc from datetime import datetime # Consume environmental forces (Gegenstress) windkraft = dc.consume.variable.windkraft() wasserkraft = dc.consume.variable.wasserkraft() # Consume anchor resources (Ankerressource) ankergewicht = dc.consume.variable.ankergewicht() ankertyp = dc.consume.variable.ankertyp() bodengrund = dc.consume.variable.bodengrund() wassertiefe = dc.consume.variable.wassertiefe_berechnet() # Consume ship data vessel_length = dc.consume.variable.vessel_length_input() vessel_width = dc.consume.variable.vessel_width_input() gross_tonnage = dc.consume.variable.gross_tonnage_input() # Consume safety factor safety_factor = dc.consume.variable.safety_factor_input() def calculate_gegenstress(windkraft, wasserkraft): """Calculate total environmental stress (Gegenstress)""" # Convert to numpy arrays if needed if hasattr(windkraft, 'values'): windkraft = windkraft.values if hasattr(wasserkraft, 'values'): wasserkraft = wasserkraft.values # Ensure same length min_len = min(len(windkraft), len(wasserkraft)) windkraft = windkraft[:min_len] wasserkraft = wasserkraft[:min_len] # Total environmental stress total_stress = windkraft + wasserkraft return { 'wind_stress': float(np.max(windkraft)) if len(windkraft) > 0 else 0, 'water_stress': float(np.max(wasserkraft)) if len(wasserkraft) > 0 else 0, 'total_stress': float(np.max(total_stress)) if len(total_stress) > 0 else 0, 'max_wind': float(np.max(windkraft)) if len(windkraft) > 0 else 0, 'max_water': float(np.max(wasserkraft)) if len(wasserkraft) > 0 else 0, 'analysis_timestamp': datetime.now().isoformat() } def calculate_ankerressource(ankergewicht, ankertyp, bodengrund, wassertiefe, vessel_length): """Calculate anchor resources (Ankerressource)""" # Anchor efficiency data (N/kg) efficiency_data = { 'Danforth (Fluke)': {'sand': 150, 'schlamm': 150, 'kiesel': 100, 'kies': 100, 'ton': 200, 'lehm': 200}, 'CQR (Plow)': {'sand': 120, 'schlamm': 100, 'kiesel': 80, 'kies': 80, 'ton': 150, 'lehm': 150}, 'Delta (Plow)': {'sand': 130, 'schlamm': 110, 'kiesel': 90, 'kies': 90, 'ton': 160, 'lehm': 160}, 'Bruce (Claw)': {'sand': 100, 'schlamm': 80, 'kiesel': 70, 'kies': 70, 'ton': 120, 'lehm': 120}, 'Rocna (Scoop)': {'sand': 140, 'schlamm': 120, 'kiesel': 100, 'kies': 100, 'ton': 180, 'lehm': 180}, 'Spade (Scoop)': {'sand': 135, 'schlamm': 115, 'kiesel': 95, 'kies': 95, 'ton': 175, 'lehm': 175}, 'Admiralty/Stock': {'sand': 130, 'schlamm': 130, 'kiesel': 60, 'kies': 60, 'ton': 175, 'lehm': 175}, 'Mushroom': {'sand': 50, 'schlamm': 200, 'kiesel': 30, 'kies': 30, 'ton': 180, 'lehm': 180}, 'Grapnel': {'sand': 80, 'schlamm': 60, 'kiesel': 70, 'kies': 70, 'ton': 100, 'lehm': 100}, 'Kedge': {'sand': 90, 'schlamm': 70, 'kiesel': 80, 'kies': 80, 'ton': 110, 'lehm': 110} } # Get efficiency for current anchor type and soil if ankertyp in efficiency_data and bodengrund in efficiency_data[ankertyp]: efficiency = efficiency_data[ankertyp][bodengrund] else: efficiency = 150 # Default efficiency # Calculate holding power holding_power = efficiency * ankergewicht # N # Calculate chain strength (simplified) chain_diameter = max(8, vessel_length / 20) # mm chain_strength = chain_diameter * chain_diameter * 0.785 * 400 # N (simplified) # Calculate scope requirements scope_ratio = 6.0 # Standard scope ratio required_scope = wassertiefe * scope_ratio return { 'anchor_weight': float(ankergewicht), 'anchor_type': str(ankertyp), 'seabed_type': str(bodengrund), 'efficiency': float(efficiency), 'holding_power': float(holding_power), 'chain_diameter': float(chain_diameter), 'chain_strength': float(chain_strength), 'water_depth': float(wassertiefe), 'required_scope': float(required_scope), 'total_capacity': float(holding_power + chain_strength) } def calculate_bruchlast(gegenstress, ankerressource, safety_factor): """Calculate breaking load (Bruchlast) - the critical limit""" total_stress = gegenstress['total_stress'] total_capacity = ankerressource['total_capacity'] # Calculate safety factor actual_safety_factor = total_capacity / total_stress if total_stress > 0 else float('inf') # Calculate breaking load breaking_load = total_capacity / safety_factor return { 'environmental_stress': total_stress, 'anchor_capacity': total_capacity, 'actual_safety_factor': actual_safety_factor, 'required_safety_factor': safety_factor, 'breaking_load': breaking_load, 'safety_status': 'SAFE' if actual_safety_factor >= safety_factor else 'DANGER' } def create_anker_situation_table(gegenstress, ankerressource, bruchlast): """Create comprehensive anchoring situation table""" # Define all parameters with consistent structure parameters = [ 'Environmental Stress (Gegenstress)', ' - Wind Force', ' - Water Force', ' - Total Environmental Force', '', 'Anchor Resources (Ankerressource)', ' - Anchor Weight', ' - Anchor Type', ' - Seabed Type', ' - Holding Power', ' - Chain Strength', ' - Total Capacity', '', 'Safety Analysis', ' - Actual Safety Factor', ' - Required Safety Factor', ' - Breaking Load', ' - Safety Status' ] values = [ f"{gegenstress['total_stress']:.0f} N", f"{gegenstress['wind_stress']:.0f} N", f"{gegenstress['water_stress']:.0f} N", f"{gegenstress['total_stress']:.0f} N", '', '', f"{ankerressource['anchor_weight']:.1f} kg", ankerressource['anchor_type'], ankerressource['seabed_type'], f"{ankerressource['holding_power']:.0f} N", f"{ankerressource['chain_strength']:.0f} N", f"{ankerressource['total_capacity']:.0f} N", '', f"{bruchlast['actual_safety_factor']:.2f}", f"{bruchlast['required_safety_factor']:.1f}", f"{bruchlast['breaking_load']:.0f} N", bruchlast['safety_status'] ] units = [ '', '', '', '', '', '', 'kg', '', '', 'N', 'N', 'N', '', '', '', 'N', '' ] # Ensure all arrays have the same length max_length = max(len(parameters), len(values), len(units)) # Pad shorter arrays with empty strings while len(parameters) < max_length: parameters.append('') while len(values) < max_length: values.append('') while len(units) < max_length: units.append('') table_data = { 'Parameter': parameters, 'Value': values, 'Unit': units } return pd.DataFrame(table_data) def generate_anker_empfehlung(gegenstress, ankerressource, bruchlast): """Generate text recommendation for anchoring""" safety_status = bruchlast['safety_status'] actual_sf = bruchlast['actual_safety_factor'] required_sf = bruchlast['required_safety_factor'] if safety_status == 'SAFE': recommendation = f""" āœ… ANCHORING SITUATION: SAFE Current environmental stress: {gegenstress['total_stress']:.0f} N Anchor capacity: {ankerressource['total_capacity']:.0f} N Safety factor: {actual_sf:.2f} (required: {required_sf:.1f}) RECOMMENDATION: Proceed with anchoring - Use {ankerressource['anchor_type']} anchor - Deploy {ankerressource['required_scope']:.0f}m of chain (6:1 scope) - Monitor conditions for changes - Anchor weight {ankerressource['anchor_weight']:.1f}kg is adequate """ else: recommendation = f""" āš ļø ANCHORING SITUATION: DANGER Current environmental stress: {gegenstress['total_stress']:.0f} N Anchor capacity: {ankerressource['total_capacity']:.0f} N Safety factor: {actual_sf:.2f} (required: {required_sf:.1f}) RECOMMENDATION: DO NOT ANCHOR - Find alternative location - Environmental forces exceed anchor capacity - Consider relocating to sheltered area - Increase anchor weight to at least {gegenstress['total_stress'] * required_sf / ankerressource['efficiency']:.1f}kg - Or wait for calmer conditions """ return recommendation # Main data product calculations print("=== MARITIME DATA PRODUCT - ANCHOR SITUATION ANALYSIS ===") # Calculate Gegenstress (Environmental Stress) gegenstress = calculate_gegenstress(windkraft, wasserkraft) print(f"Environmental Stress: {gegenstress['total_stress']:.0f} N") # Calculate Ankerressource (Anchor Resources) ankerressource = calculate_ankerressource(ankergewicht, ankertyp, bodengrund, wassertiefe, vessel_length) print(f"Anchor Capacity: {ankerressource['total_capacity']:.0f} N") # Calculate Bruchlast (Breaking Load) bruchlast = calculate_bruchlast(gegenstress, ankerressource, safety_factor) print(f"Safety Factor: {bruchlast['actual_safety_factor']:.2f}") # Create anchoring situation table anker_situation_table = create_anker_situation_table(gegenstress, ankerressource, bruchlast) # Generate anchoring recommendation anker_empfehlung = generate_anker_empfehlung(gegenstress, ankerressource, bruchlast) # Publish results dc.publish.variable('gegenstress', gegenstress) dc.publish.variable('ankerressource', ankerressource) dc.publish.variable('bruchlast', bruchlast) dc.publish.variable('anker_situation_table', anker_situation_table.to_dict('records')) dc.publish.variable('anker_empfehlung', anker_empfehlung) **Publish Environmental Stress card**
# First publish the variable # Publish Environmental Stress card dc.publish.card( type='text', label='Environmental Stress (Gegenstress)', text=f"Wind: {gegenstress['wind_stress']:.0f} N<br/>Water: {gegenstress['water_stress']:.0f} N<br/>Total: {gegenstress['total_stress']:.0f} N", variable_key='gegenstress_display', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (4,0), "deck": "default-deck"}] )

Publish Anchor Resources card

# First publish the variable dc.publish.card( type='text', label='Anchor Resources (Ankerressource)', text=f"Weight: {ankerressource['anchor_weight']:.1f} kg<br/>Type: {ankerressource['anchor_type']}<br/>Seabed: {ankerressource['seabed_type']}<br/>Holding Power: {ankerressource['holding_power']:.0f} N<br/>Chain Strength: {ankerressource['chain_strength']:.0f} N<br/>Total Capacity: {ankerressource['total_capacity']:.0f} N", variable_key='ankerressource_display', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (7,0), "deck": "default-deck"}] )
# Publish Safety Analysis card dc.publish.card( type='text', label='Safety Analysis', text=f"Actual Safety Factor: {bruchlast['actual_safety_factor']:.2f}<br/>Required Safety Factor: {bruchlast['required_safety_factor']:.1f}<br/>Breaking Load: {bruchlast['breaking_load']:.0f} N<br/>Status: {bruchlast['safety_status']}", variable_key='safety_analysis_display', logic_view_size=(2,1), layout=[{"size": (3,2), "position": (10,0), "deck": "default-deck"}] ) # Publish Anchoring Recommendation card # Clean up the recommendation text for DataCards display recommendation_clean = anker_empfehlung.replace('\n', '<br/>').strip() recommendation_short = recommendation_clean[:300] + "..." if len(recommendation_clean) > 300 else recommendation_clean
dc.publish.card( type='text', label='Anchoring Recommendation', text=recommendation_short, variable_key='anchoring_recommendation_display', logic_view_size=(2,1), layout=[{"size": (6,3), "position": (4,2), "deck": "default-deck"}] )
Last updated on