simulation_analysis.results

Creates output logs and plots for interpreting and analyzing results.

   1"""
   2Creates output logs and plots for interpreting and analyzing results.
   3"""
   4
   5import os
   6import re
   7import math
   8
   9import pandas as pd
  10import matplotlib.animation as animation
  11import matplotlib.pyplot as plt
  12import matplotlib.colors as mcolors
  13import numpy as np
  14from tqdm import tqdm
  15
  16import constants
  17from simulation_handler.helpers import log_line
  18
  19
  20nan = float('nan')
  21
  22#######################
  23# Process charts
  24#######################
  25
  26
  27def plot_process_chart(events, run_id):
  28    """
  29    # TODO: Ensure this is compatible with the new event structure.
  30    Generates process charts for each terminal based on the events.
  31    Args:
  32        events (list): List of events where each event is a tuple containing:
  33                       (event_name, terminal_type, terminal_id, event_type, time, additional_info)
  34        run_id (str): Unique identifier for the run to save the plots.
  35    Returns:
  36        None: Saves process charts as images in the specified directory.
  37    """
  38    terminal_events = {}
  39
  40    output_dir = f'.{run_id}/plots/TerminalProcessCharts'
  41    if not os.path.exists(output_dir):
  42        os.makedirs(output_dir)
  43
  44    for event in events:
  45        try:
  46            _, terminal_type, terminal_id, event_type, time, _ = event
  47            key = (terminal_type, terminal_id)
  48            if key not in terminal_events:
  49                terminal_events[key] = {}
  50            terminal_events[key].setdefault(event_type, []).append(time)
  51        except:
  52            if event[0] != "Truck Arrival" and event[0] != "Truck Departure":
  53                print(event)
  54            pass
  55
  56    for (terminal_type, terminal_id), events in terminal_events.items():
  57        try:
  58            df = pd.DataFrame()
  59            df['Time'] = events['arrive'] + events['dock'] + \
  60                sum([events[e] for e in events if e not in [
  61                    'arrive', 'dock', 'depart']], []) + events['depart']
  62            df['Event'] = ['Ship Arrival'] * len(events['arrive']) + ['Ship Dock'] * len(events['dock']) + sum([[e] * len(
  63                events[e]) for e in events if e not in ['arrive', 'dock', 'depart']], []) + ['Ship Depart'] * len(events['depart'])
  64
  65            plt.figure(figsize=(10, 6))
  66            plt.scatter(df['Time'], df['Event'])
  67            plt.title(
  68                f'Process Chart for {terminal_type} Terminal {terminal_id}')
  69            plt.xlabel('Time')
  70            plt.ylabel('Event')
  71
  72            plt.savefig(
  73                f'{output_dir}/process_chart_{terminal_type}_{terminal_id}.jpg')
  74            plt.close()
  75        except KeyError:
  76            pass
  77
  78#######################
  79# Dwell times and Turn times
  80#######################
  81
  82
  83def plot_dwell_times(events, run_id):
  84    """
  85    Generates plots for dwell times and turn times of ships at terminals.
  86    Args:
  87        events (list): List of events where each event is a tuple containing:
  88                       (ship_name, terminal_type, terminal_id, event_type, time, additional_info)
  89        run_id (str): Unique identifier for the run to save the plots.
  90    Returns:
  91        None: Saves plots as images in the specified directory.
  92    """
  93
  94    all_dwell_times = []
  95    all_turn_times = []
  96
  97    ship_arrival_times = []
  98    ship_dock_times = []
  99    ship_undock_times = []
 100    ship_departure_times = []
 101
 102    dwell_times = {}
 103    turn_times = {}
 104
 105    # read the events
 106    for event in events:
 107        try:
 108            name, terminal_type, terminal_id, event_type, time, _ = event
 109            if event_type == "arrive":  # arrive at anchorage
 110                ship_arrival_times.append(
 111                    (name, terminal_type, terminal_id, time))
 112            if event_type == "dock":
 113                ship_dock_times.append(
 114                    (name, terminal_type, terminal_id, time))
 115            if event_type == "undock":
 116                ship_undock_times.append(
 117                    (name, terminal_type, terminal_id, time))
 118            elif event_type == "depart":
 119                ship_departure_times.append(
 120                    (name, terminal_type, terminal_id, time))
 121        except:
 122            pass
 123
 124    # Calculate turn times
 125    for ship, terminal_type, terminal_id, arrival_time in ship_arrival_times:
 126        for name, t_type, t_id, departure_time in ship_departure_times:
 127            if ship == name and terminal_type == t_type and terminal_id == t_id:
 128                key = (terminal_type, terminal_id)
 129                if key not in turn_times:
 130                    turn_times[key] = {}
 131                turn_times[key][ship] = departure_time - arrival_time
 132                all_turn_times.append(departure_time - arrival_time)
 133
 134    # Calculate dwell times
 135    for ship, terminal_type, terminal_id, dock_time in ship_dock_times:
 136        for name, t_type, t_id, undock_time in ship_undock_times:
 137            if ship == name and terminal_type == t_type and terminal_id == t_id:
 138                key = (terminal_type, terminal_id)
 139                if key not in dwell_times:
 140                    dwell_times[key] = {}
 141                dwell_times[key][ship] = undock_time - dock_time
 142                all_dwell_times.append(undock_time - dock_time)
 143
 144    # Create bar plot of the dwell times for individual vessels, by unique terminal
 145    for (terminal_type, terminal_id), ships_dwell_times in dwell_times.items():
 146        try:
 147            df = pd.DataFrame(ships_dwell_times.items(),
 148                              columns=['Ship', 'Dwell Time'])
 149            # Set 'Ship' as index for proper labeling on x-axis
 150            df.set_index('Ship', inplace=True)
 151            df.plot(kind='bar', y='Dwell Time', legend=False,
 152                    title=f'Dwell times of ships at {terminal_type} terminal {terminal_id}')
 153            plt.ylabel('Dwell time (hr)')
 154            plt.savefig(
 155                f'.{run_id}/plots/DwellTimes/dwell_times_{terminal_type}_{terminal_id}.jpg')
 156            plt.close()  # Clear the figure after saving the plot
 157        except Exception as e:
 158            print(
 159                f"Error in dwell time plot generation for {terminal_type} terminal {terminal_id}: {e}")
 160            pass
 161
 162    # Create bar plot of the turn times for individual vessels, by unique terminal
 163    for (terminal_type, terminal_id), ships_turn_times in turn_times.items():
 164        try:
 165            df = pd.DataFrame(ships_turn_times.items(),
 166                              columns=['Ship', 'Turn Time'])
 167            # Set 'Ship' as index for proper labeling on x-axis
 168            df.set_index('Ship', inplace=True)
 169            df.plot(kind='bar', y='Turn Time', legend=False,
 170                    title=f'Turn Times of Ships at {terminal_type} Terminal {terminal_id}')
 171            plt.ylabel('Turn Time')
 172            plt.savefig(
 173                f'.{run_id}/plots/TurnTimes/turn_times_{terminal_type}_{terminal_id}.jpg')
 174            plt.close()  # Clear the figure after saving the plot
 175        except Exception as e:
 176            print(
 177                f"Error in turn time plot generation for {terminal_type} Terminal {terminal_id}: {e}")
 178            pass
 179
 180    # Create histogram of the dwell times of all vessels visiting each unique terminal
 181    for (terminal_type, terminal_id), ships_dwell_times in dwell_times.items():
 182        try:
 183            dwell_times_list = list(ships_dwell_times.values())
 184            plt.figure(figsize=(10, 6))
 185            plt.hist(dwell_times_list, bins=20, edgecolor='black')
 186            plt.title(
 187                f'Dwell time distribution at {terminal_type} terminal {terminal_id}')
 188            plt.xlabel('Dwell time (hr)')
 189            plt.ylabel('Frequency')
 190            plt.savefig(
 191                f'.{run_id}/plots/DwellTimesDist/dwell_distribution_{terminal_type}_{terminal_id}.jpg')
 192            plt.close()
 193        except Exception as e:
 194            print(
 195                f"Error in dwell time distribution plot generation for {terminal_type} terminal {terminal_id}: {e}")
 196            pass
 197
 198    # Create histogram of the turn times of all vessels visiting each unique terminal
 199    for (terminal_type, terminal_id), ships_turn_times in turn_times.items():
 200        try:
 201            turn_times_list = list(ships_turn_times.values())
 202            plt.figure(figsize=(10, 6))
 203            plt.hist(turn_times_list, bins=20, edgecolor='black')
 204            plt.title(
 205                f'Turn time distribution at {terminal_type} terminal {terminal_id}', fontsize=20)
 206            plt.xlabel('Turn time (hr)', fontsize=16)
 207            plt.ylabel('Frequency', fontsize=16)
 208            plt.savefig(
 209                f'.{run_id}/plots/TurnTimesDist/turn_distribution_{terminal_type}_{terminal_id}.jpg')
 210            plt.close()
 211        except Exception as e:
 212            print(
 213                f"Error in turn time distribution plot generation for {terminal_type} terminal {terminal_id}: {e}")
 214            pass
 215
 216    # Create histogram of dwell times for all vessels in port system (all terminals)
 217    all_dwell_times = [time for terminal_dwell_times in dwell_times.values(
 218    ) for time in terminal_dwell_times.values()]
 219    plt.figure(figsize=(10, 6))
 220    plt.hist(all_dwell_times, bins=20, edgecolor='black')
 221    plt.title('Overall vessel terminal dwell time distribution', fontsize=20)
 222    plt.xlabel('Dwell time (hr)', fontsize=16)
 223    plt.ylabel('Frequency', fontsize=16)
 224    plt.xticks(fontsize=12)
 225    plt.yticks(fontsize=12)
 226    plt.savefig(f'.{run_id}/plots/dwell_time_distribution.jpg')
 227    plt.close()
 228
 229    # Create histogram of turn times for all vessels in port system (all terminals)
 230    all_turn_times = [time for terminal_turn_times in turn_times.values()
 231                      for time in terminal_turn_times.values()]
 232    plt.figure(figsize=(10, 6))
 233    plt.hist(all_turn_times, bins=20, edgecolor='black')
 234    plt.title('Overall vessel turn time distribution', fontsize=20)
 235    plt.xlabel('Turn time (hr)', fontsize=16)
 236    plt.ylabel('Frequency', fontsize=16)
 237    plt.xticks(fontsize=12)
 238    plt.yticks(fontsize=12)
 239    plt.savefig(f'.{run_id}/plots/turn_time_distribution.jpg')
 240    plt.close()
 241
 242    report_path = f".{run_id}/logs/final_report.txt"
 243    with open(report_path, 'a') as f:
 244        f.write("Dwell Time Analysis:\n")
 245        f.write(f"Max dwell time: {max(all_dwell_times)}\n")
 246        f.write(
 247            f"Median dwell time: {sorted(all_dwell_times)[len(all_dwell_times) // 2]}\n")
 248        f.write(
 249            f"Average dwell time: {sum(all_dwell_times) / len(all_dwell_times)}\n\n")
 250
 251        f.write("Turn Time Analysis:\n")
 252        f.write(f"Max turn time: {max(all_turn_times)}\n")
 253        f.write(
 254            f"Median turn time: {sorted(all_turn_times)[len(all_turn_times) // 2]}\n")
 255        f.write(
 256            f"Average turn time: {sum(all_turn_times) / len(all_turn_times)}\n\n")
 257
 258    return None
 259
 260
 261def analyze_turn_time_ships(run_id):
 262    """ Analyze the turn times and dwell times of ships from the Excel file and save the results to a report.
 263    Args:
 264        run_id (str): Unique identifier for the run to save the report.
 265    Returns:
 266        None: Saves the analysis results to a text file in the specified directory.
 267    """
 268    # Load the Excel file
 269    file_path = f'.{run_id}/logs/ship_logs.xlsx'
 270
 271    df = pd.read_excel(file_path)
 272    mean_dwell_times = df.groupby(['Terminal Directed'])[
 273        'Turn Time'].mean().reset_index()
 274    with open(f".{run_id}/logs/final_report.txt", 'a') as f:
 275        f.write("Mean Turn Times by Terminal Directed:\n")
 276        f.write(str(mean_dwell_times) + "\n\n")
 277    mean_dwell_times = df.groupby(['Terminal Type'])[
 278        'Turn Time'].mean().reset_index()
 279    with open(f".{run_id}/logs/final_report.txt", 'a') as f:
 280        f.write("Mean Turn Times by Terminal Type:\n")
 281        f.write(str(mean_dwell_times) + "\n\n")
 282
 283    mean_dwell_times = df.groupby(['Terminal Directed'])[
 284        'Dwell Time'].mean().reset_index()
 285    with open(f".{run_id}/logs/final_report.txt", 'a') as f:
 286        f.write("Mean Dwell Times by Terminal Directed:\n")
 287        f.write(str(mean_dwell_times) + "\n\n")
 288    mean_dwell_times = df.groupby(['Terminal Type'])[
 289        'Dwell Time'].mean().reset_index()
 290    with open(f".{run_id}/logs/final_report.txt", 'a') as f:
 291        f.write("Mean Dwell Times by Terminal Type:\n")
 292        f.write(str(mean_dwell_times) + "\n\n")
 293
 294
 295#######################
 296# Queues
 297#######################
 298
 299def plot_queues(SHIPS_IN_CHANNEL_TRACK, name, place, run_id):
 300    """
 301    Plots the number of ships in the channel over time.
 302    Args:
 303        SHIPS_IN_CHANNEL_TRACK (list): List of tuples containing time and number of ships in the channel.
 304        name (str): Name of the ship type (e.g., "Container", "Liquid", "Dry Bulk").
 305        place (str): Place where the ships are tracked (e.g., "Port").
 306        run_id (str): Unique identifier for the run to save the plot.
 307    Returns:    
 308        None: Saves the plot as a PDF file in the specified directory.
 309    """
 310
 311    # Plotting
 312    plt.figure(figsize=(12, 8))
 313
 314    x = [i[0] for i in SHIPS_IN_CHANNEL_TRACK]
 315    y = [i[1] for i in SHIPS_IN_CHANNEL_TRACK]
 316    plt.plot(x, y)
 317    plt.suptitle(f'Number of {name} over time', fontsize=20)
 318    plt.title(f'at {place}', fontsize=20)
 319    plt.xlabel('Time', fontsize=15)
 320    plt.ylabel(f'Number of {name}', fontsize=15)
 321    plt.xticks(fontsize=12)
 322    plt.yticks(fontsize=12)
 323    plt.tight_layout()
 324    plt.xlim(min(x)-1, max(x)+2)
 325    plt.ylim(min(y)-1, max(y)+2)
 326    plt.savefig(
 327        f'.{run_id}/plots/{name}in{place}OverTime{constants.mean_interarrival_time_total}.pdf')
 328
 329    report_path = "./logs/final_report.txt"
 330    with open(report_path, 'a') as f:
 331        f.write(f"Mean number of {name} in {place}: {sum(y) / len(y)}\n\n")
 332
 333    plt.close()
 334
 335
 336#######################
 337# Resources
 338#######################
 339
 340
 341def track_utilization(container_data, liquid_data, drybulk_data, run_id):
 342    """
 343    Calculates the mean utilization of resources (berths and yards) for container, liquid, and dry bulk terminals.
 344    Args:
 345        container_data (pd.DataFrame): DataFrame containing container terminal data.
 346        liquid_data (pd.DataFrame): DataFrame containing liquid terminal data.
 347        drybulk_data (pd.DataFrame): DataFrame containing dry bulk terminal data.
 348        run_id (str): Unique identifier for the run to save the results.
 349    Returns:
 350        list: A list containing the mean utilization of resources for each terminal type.
 351    """
 352
 353    track_list = []
 354
 355    excel_data = f'.{run_id}/logs/availability.xlsx'
 356
 357    def calculate_mean_utilization(data, resource_type):
 358        available_columns = [
 359            col for col in data.columns if 'Available' in col and resource_type in col]
 360        used_columns = [
 361            col for col in data.columns if 'Used' in col and resource_type in col]
 362
 363        utilization = {}
 364        for available, used in zip(available_columns, used_columns):
 365            terminal_name = available.split('_Available_')[0]
 366            utilization[terminal_name] = data[used].sum(
 367            ) / (data[available].sum() + data[used].sum())
 368
 369        overall_utilization = sum(data[used_columns].sum(
 370        )) / (sum(data[available_columns].sum()) + sum(data[used_columns].sum()))
 371
 372        return utilization, overall_utilization
 373
 374    container_berth_utilization, container_berth_overall = calculate_mean_utilization(
 375        container_data, 'Berth_Ctr')
 376    container_yard_utilization, container_yard_overall = calculate_mean_utilization(
 377        container_data, 'Yard')
 378
 379    liquid_berth_utilization, liquid_berth_overall = calculate_mean_utilization(
 380        liquid_data, 'Berth_liq')
 381    liquid_tank_utilization, liquid_tank_overall = calculate_mean_utilization(
 382        liquid_data, 'Tank')
 383
 384    drybulk_berth_utilization, drybulk_berth_overall = calculate_mean_utilization(
 385        drybulk_data, 'Berth_db')
 386    drybulk_silo_utilization, drybulk_silo_overall = calculate_mean_utilization(
 387        drybulk_data, 'Silo')
 388
 389    track_list = [container_berth_utilization, container_berth_overall, container_yard_utilization, container_yard_overall, liquid_berth_utilization,
 390                  liquid_berth_overall, liquid_tank_utilization, liquid_tank_overall, drybulk_berth_utilization, drybulk_berth_overall, drybulk_silo_utilization, drybulk_silo_overall]
 391
 392    return track_list
 393
 394
 395def get_utilization(container_data, liquid_data, drybulk_data, run_id):
 396    """
 397    Calculates the mean utilization of resources (berths and yards) for container, liquid, and dry bulk terminals.
 398    Args:
 399        container_data (pd.DataFrame): DataFrame containing container terminal data.
 400        liquid_data (pd.DataFrame): DataFrame containing liquid terminal data.
 401        drybulk_data (pd.DataFrame): DataFrame containing dry bulk terminal data.
 402        run_id (str): Unique identifier for the run to save the results.
 403    Returns:
 404        list: A list containing the mean utilization of resources for each terminal type.
 405    """
 406
 407    track_list = []
 408
 409    excel_data = f'.{run_id}/logs/availability.xlsx'
 410
 411    def calculate_mean_utilization(data, resource_type):
 412        available_columns = [
 413            col for col in data.columns if 'Available' in col and resource_type in col]
 414        used_columns = [
 415            col for col in data.columns if 'Used' in col and resource_type in col]
 416        # print("used:", used_columns)
 417        utilization = {}
 418        for available, used in zip(available_columns, used_columns):
 419            terminal_name = available.split('_Available_')[0]
 420            # print("data", data)
 421            # print("used:", used)
 422            # print(data[used])
 423            # print(list(data[used]))
 424            utilization[terminal_name] = list(
 425                data[used])[-1] / (list(data[available])[-1] + list(data[used])[-1])
 426        overall_utilization = data[used_columns].iloc[-1].sum() / (
 427            data[available_columns].iloc[-1].sum() + data[used_columns].iloc[-1].sum())
 428
 429        return utilization, overall_utilization
 430
 431    container_berth_utilization, container_berth_overall = calculate_mean_utilization(
 432        container_data, 'Berth_Ctr')
 433    container_yard_utilization, container_yard_overall = calculate_mean_utilization(
 434        container_data, 'Yard')
 435
 436    liquid_berth_utilization, liquid_berth_overall = calculate_mean_utilization(
 437        liquid_data, 'Berth_liq')
 438    liquid_tank_utilization, liquid_tank_overall = calculate_mean_utilization(
 439        liquid_data, 'Tank')
 440
 441    drybulk_berth_utilization, drybulk_berth_overall = calculate_mean_utilization(
 442        drybulk_data, 'Berth_db')
 443    drybulk_silo_utilization, drybulk_silo_overall = calculate_mean_utilization(
 444        drybulk_data, 'Silo')
 445
 446    track_list = [container_berth_utilization, container_berth_overall, container_yard_utilization, container_yard_overall, liquid_berth_utilization,
 447                  liquid_berth_overall, liquid_tank_utilization, liquid_tank_overall, drybulk_berth_utilization, drybulk_berth_overall, drybulk_silo_utilization, drybulk_silo_overall]
 448
 449    return track_list
 450
 451
 452def add_to_report(report_lines, resource, utilization, overall_utilization):
 453    """
 454    Adds the mean utilization of a resource to the report lines.
 455    Args:
 456        report_lines (list): List of report lines to which the utilization will be added.
 457        resource (str): Name of the resource (e.g., "Container berth", "Liquid storage").
 458        utilization (dict): Dictionary containing the mean utilization for each terminal.
 459        overall_utilization (float): Overall mean utilization for the resource.
 460    Returns:
 461        None: Appends the utilization information to the report lines.
 462    """
 463
 464    report_lines.append(f"Mean {resource} Utilization:")
 465    for terminal, mean_util in utilization.items():
 466        report_lines.append(f"{terminal}: {mean_util:.2%}")
 467    report_lines.append(
 468        f"Overall {resource} Utilization: {overall_utilization:.2%}")
 469    report_lines.append("\n")
 470
 471
 472def save_track_list(run_id, timestep, track_list):
 473    """
 474    Saves the utilization track list to a text file for the given timestep.
 475    Args:
 476        run_id (str): Unique identifier for the run to save the report.
 477        timestep (int): The current timestep for which the report is generated.
 478        track_list (list): List containing the mean utilization of resources for each terminal type.
 479    Returns:
 480        None: Saves the report to a text file in the specified directory.
 481    """
 482
 483    container_berth_utilization, container_berth_overall, container_yard_utilization, container_yard_overall, liquid_berth_utilization, liquid_berth_overall, liquid_tank_utilization, liquid_tank_overall, drybulk_berth_utilization, drybulk_berth_overall, drybulk_silo_utilization, drybulk_silo_overall = track_list
 484    report_lines = []
 485
 486    # Container Terminals
 487    add_to_report(report_lines, "Container berth",
 488                  container_berth_utilization, container_berth_overall)
 489    add_to_report(report_lines, "Container storage",
 490                  container_yard_utilization, container_yard_overall)
 491
 492    # Liquid Terminals
 493    add_to_report(report_lines, "Liquid berth",
 494                  liquid_berth_utilization, liquid_berth_overall)
 495    add_to_report(report_lines, "Liquid storage",
 496                  liquid_tank_utilization, liquid_tank_overall)
 497
 498    # Dry Bulk Terminals
 499    add_to_report(report_lines, "Drybulk berth",
 500                  drybulk_berth_utilization, drybulk_berth_overall)
 501    add_to_report(report_lines, "Drybulk storage",
 502                  drybulk_silo_utilization, drybulk_silo_overall)
 503
 504    # Save report to text file
 505    report_path = f'.{run_id}/logs/availability/{timestep}.txt'
 506    with open(report_path, 'a') as file:
 507        file.write(f"Time Step: {timestep}\n")
 508        file.write("\n".join(report_lines))
 509
 510
 511#######################
 512# Train
 513#######################
 514
 515def gen_train_df(train_events, run_id):
 516    """
 517    Generates a DataFrame from the train events and saves it to a CSV file.
 518    Args:
 519        train_events (dict): Dictionary containing train events where keys are event names and values are lists of event data.
 520        run_id (str): Unique identifier for the run to save the DataFrame.
 521    Returns:
 522        pd.DataFrame: DataFrame containing the train events.
 523    """
 524    train_events_df = pd.DataFrame(train_events).T
 525    train_events_df.to_csv(f'.{run_id}/logs/train_logs.csv')
 526    return train_events_df
 527
 528
 529#######################
 530# Time distribution
 531#######################
 532
 533def get_dists(run_id, plot=True):
 534    """
 535    Analyzes the ship logs to calculate the mean, min, max, and standard deviation of various time distributions for each terminal type.
 536    Args:
 537        run_id (str): Unique identifier for the run to save the analysis results.
 538        plot (bool): Whether to generate and save plots of the time distributions.
 539    Returns:
 540        tuple: A tuple containing the mean, min, max, and standard deviation DataFrames for each terminal type.
 541    """
 542    def extract_t(restriction_str):
 543        if isinstance(restriction_str, str):
 544            parts = restriction_str.split(", ")
 545            for part in parts:
 546                if part.startswith("T"):
 547                    return float(part.split(":")[1])
 548        return 0.0
 549
 550    data = pd.read_excel(f'.{run_id}/logs/ship_logs.xlsx', sheet_name='Sheet1')
 551
 552    data['Restriction In (T)'] = data['Time for Restriction In'].apply(
 553        extract_t)
 554    data['Restriction Out (T)'] = data['Time for Restriction Out'].apply(
 555        extract_t)
 556
 557    data['Channel In'] = data['Time to Common Channel In'] + \
 558        data['Time to Travel Channel In']
 559    data['Channel Out'] = data['Time to Common Channel Out'] + \
 560        data['Time to Travel Channel Out']
 561    data['Terminal Ops'] = data['Loading Time'] + data['Unloading Time']
 562    data['Terminal Other'] = data['Waiting Time'] + data['Departure Time']
 563
 564    columns = [
 565        'Time to get Berth', 'Restriction In (T)', 'Channel In', 'Time to get Pilot In',
 566        'Time to get Tugs In', 'Terminal Ops', 'Terminal Other', 'Restriction Out (T)',
 567        'Time to get Pilot Out', 'Time to get Tugs Out', 'Channel Out'
 568    ]
 569
 570    mean_values = data.groupby('Terminal Type')[columns].mean()
 571    min_values = data.groupby('Terminal Type')[columns].min()
 572    max_values = data.groupby('Terminal Type')[columns].max()
 573    std_values = data.groupby('Terminal Type')[columns].std()
 574
 575    error_bars = {'min': mean_values - min_values,
 576                  'max': max_values - mean_values}
 577
 578    with open(f'.{run_id}/logs/final_report.txt', 'a') as file:
 579        file.write("Mean Time Distribution:\n")
 580        file.write(mean_values.to_string())
 581        file.write("\n\nMin Time Distribution:\n")
 582        file.write(min_values.to_string())
 583        file.write("\n\nMax Time Distribution:\n")
 584        file.write(max_values.to_string())
 585        file.write("\n\nStandard Deviation:\n")
 586        file.write(std_values.to_string())
 587
 588    if plot:
 589        for terminal in mean_values.index:
 590            plt.figure(figsize=(10, 6))
 591            means = mean_values.loc[terminal]
 592            min_err = error_bars['min'].loc[terminal]
 593            max_err = error_bars['max'].loc[terminal]
 594            stds = std_values.loc[terminal]
 595
 596            plt.errorbar(columns, means, yerr=[
 597                         min_err, max_err], fmt='-o', capsize=5)
 598
 599            for i, (mean, min_e, max_e, std) in enumerate(zip(means, min_err, max_err, stds)):
 600                plt.annotate(
 601                    f'Mean: {mean:.1f}\nSD: {std:.1f}',
 602                    (i, mean),
 603                    textcoords="offset points",
 604                    xytext=(0, 10),
 605                    ha='center'
 606                )
 607
 608            plt.xticks(rotation=90)
 609            plt.title(f'{terminal} Terminal: Mean with Min/Max and SD')
 610            plt.xlabel('Processes')
 611            plt.ylabel('Time (Units)')
 612            plt.tight_layout()
 613            # os.makedirs('/plots/TerminalProcessDist/', exist_ok=True)
 614            plt.savefig(
 615                f'.{run_id}/plots/TerminalProcessDist/{terminal}_terminal_plot.png')
 616            plt.close()
 617    return mean_values, min_values, max_values, std_values
 618
 619
 620#######################
 621# Channel and Anchorage
 622#######################
 623
 624
 625def plot_channel(run_id):
 626    """
 627    Plots the channel sections and their respective terminals.
 628    Args:
 629        run_id (str): Unique identifier for the run to save the plot.
 630    Returns:
 631        None: Saves the plot as a PDF file in the specified directory.
 632    """
 633
 634    # Normalize the draft values for color mapping
 635    channel_sections = constants.CHANNEL_SECTION_DIMENSIONS
 636    last_section_dict = constants.LAST_SECTION_DICT
 637    drafts = [draft for _, _, draft, _ in channel_sections.values()]
 638    norm = mcolors.Normalize(vmin=min(drafts), vmax=max(drafts))
 639    cmap = plt.cm.viridis
 640    fig, ax = plt.subplots(figsize=(20, 8))
 641    max_width = max(width for _, width, _, _ in channel_sections.values())
 642
 643    # Calculate cumulative lengths for x positions (rotated plot)
 644    cumulative_length = 0
 645    x_positions = {}
 646    for section, (length, width, draft, speed) in channel_sections.items():
 647        x_positions[section] = cumulative_length + \
 648            length / 2  # Middle of the section
 649        cumulative_length += length
 650
 651    for section, (length, width, draft, speed) in channel_sections.items():
 652        color = cmap(norm(draft))
 653        y_pos = (max_width - width) / 2  # Center-align the rectangle
 654        ax.add_patch(plt.Rectangle(
 655            (x_positions[section] - length / 2, y_pos), length, width, edgecolor='black', facecolor=color))
 656        terminal_num = next((f'{terminal_type} Terminal : #{terminal}' for terminal_type, terminals in last_section_dict.items(
 657        ) for terminal, sec in terminals.items() if sec == section), None)
 658        annotation = f'Section {section}'
 659        if terminal_num:
 660            annotation += f'\n{terminal_num}'
 661        ax.text(x_positions[section], max_width / 2, annotation,
 662                ha='center', va='center', color='white', fontsize=8, rotation=90)
 663
 664    cbar = plt.colorbar(plt.cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax)
 665    cbar.set_label('Depth (Draft)')
 666
 667    ax.set_xlim(0, cumulative_length)
 668    ax.set_ylim(0, max_width + 5)
 669    ax.set_xlabel('Length')
 670    ax.set_ylabel('Breadth')
 671    ax.set_title('Channel Sections and Terminals')
 672    plt.savefig(f'.{run_id}/plots/ChannelSections.pdf')
 673    plt.close()
 674
 675
 676def ship_data_csv_1(run_id):
 677    """
 678    Parses the ship data from the log file and saves it to a CSV file. 
 679    The CSV file contains information about each ship's entry and exit times in the channel sections.
 680    Args:
 681        run_id (str): Unique identifier for the run to save the CSV file.
 682    Returns:
 683        pd.DataFrame: DataFrame containing the parsed ship data.
 684    """
 685    ship_data = []
 686    file_path = f'.{run_id}/logs/ships_in_channel.txt'
 687
 688    with open(file_path, 'r') as file:
 689        for line in file:
 690            match = re.search(
 691                r"Ship (\d+) of type (\w+) of width ([\d.]+) and draft ([\d.]+) spent from ([\d.]+) to ([\d.]+) in section (\d+) going (\w+)", line)
 692            if match:
 693                ship_data.append({
 694                    "ship": int(match.group(1)),
 695                    "ship_type": match.group(2),
 696                    "width": float(match.group(3)),
 697                    "draft": float(match.group(4)),
 698                    "section": int(match.group(7)),
 699                    "start": float(match.group(5)),
 700                    "end": float(match.group(6)),
 701                    "direction": match.group(8)
 702                })
 703            else:
 704                print(f"Error parsing line: {line}")
 705
 706    df = pd.DataFrame(ship_data)
 707    df = df.sort_values(by=['ship', 'direction', 'section'])
 708
 709    df.to_csv(
 710        f'.{run_id}/logs/channel_section_entry_exit_times.csv', index=False)
 711
 712    return df
 713
 714
 715def ship_data_csv_2(run_id):
 716    """
 717    Parses the ship data from the log file and saves it to a CSV file with time spent in each section.
 718    Args:
 719        run_id (str): Unique identifier for the run to save the CSV file.
 720    Returns:
 721        pd.DataFrame: DataFrame containing the parsed ship data with time spent in each section.
 722    """
 723    ship_data = {}
 724    file_path = f'.{run_id}/logs/ships_in_channel.txt'
 725    pattern = r"Ship (\d+) of type (\w+) of width ([\d.]+) and draft ([\d.]+) spent from ([\d.]+) to ([\d.]+) in section (\d+) going (\w+)"
 726
 727    with open(file_path, 'r') as file:
 728        for line in file:
 729            match = re.search(pattern, line)
 730            if match:
 731                ship_num = int(match.group(1))
 732                ship_dir = match.group(8)
 733                ship_type = match.group(2)
 734                section = int(match.group(7))
 735                time_spent = float(match.group(6)) - float(match.group(5))
 736
 737                if (ship_num, ship_dir) not in ship_data:
 738                    ship_data[(ship_num, ship_dir)] = {}
 739
 740                ship_data[(ship_num, ship_dir)][section] = time_spent
 741            else:
 742                print(f"Error parsing line: {line}")
 743
 744        max_section = max(max(ship_info.keys())
 745                          for ship_info in ship_data.values())
 746        df = pd.DataFrame(ship_data).T
 747        df = df.reindex(columns=range(max_section + 1)).fillna(np.nan)
 748        df.columns = [f"Time in Sec {i}" for i in range(max_section + 1)]
 749        df['Total Time in Channel'] = df.sum(axis=1)
 750        df['Ship'] = [ship[0] for ship in df.index]
 751        df['Direction'] = [ship[1] for ship in df.index]
 752        section_columns = [f"Time in Sec {i}" for i in range(max_section + 1)]
 753        df = df[['Ship', 'Direction'] +
 754                ['Total Time in Channel'] + section_columns]
 755        df = df.sort_values(by=['Ship', 'Direction'])
 756        df.to_csv(f'.{run_id}/logs/ships_time_in_channel.csv', index=False)
 757
 758        return df
 759
 760
 761def ships_in_channel_analysis(run_id, plot=True):
 762    """
 763    Analyzes the ships in the channel by reading the ship data from a text file, calculating the time spent in each section, and generating a time series of ships in the channel.
 764    Args:
 765        run_id (str): Unique identifier for the run to save the analysis results.
 766        plot (bool): Whether to generate and save plots of the channel utilization.
 767    Returns:
 768        None: Saves the analysis results to CSV files and generates plots if `plot` is True.
 769    """
 770
 771    ship_data_csv_1(run_id)
 772    ship_data_csv_2(run_id)
 773
 774    os.remove(f'.{run_id}/logs/ships_in_channel.txt')
 775
 776    file_path = f'.{run_id}/logs/channel_section_entry_exit_times.csv'
 777    ships_data = pd.read_csv(file_path)
 778    ships_data['time_in_section'] = ships_data['end'] - ships_data['start']
 779    max_time = ships_data['end'].max()
 780
 781    section_time_series = pd.DataFrame(0, index=np.arange(
 782        max_time)-1, columns=range(ships_data['section'].max() + 1))
 783
 784    for _, row in ships_data.iterrows():
 785        section_time_series.loc[row['start']:row['end'], row['section']] += 1
 786
 787    # save section time series to csv
 788    section_time_series.to_csv(f'.{run_id}/logs/section_time_series.csv')
 789    total_ships_in_channel_full = section_time_series.sum(axis=1)
 790    mean_ships_per_time_step = section_time_series.loc[constants.WARMUP_ITERS:].mean(
 791    )
 792    total_ships_in_channel = section_time_series.sum(axis=1)
 793    mean_total_ships_in_channel = total_ships_in_channel.mean()
 794
 795    # save total ships in channel to csv
 796    total_ships_in_channel.to_csv(f'.{run_id}/logs/total_ships_in_channel.csv')
 797
 798    if plot:
 799        plt.plot(total_ships_in_channel_full)
 800        plt.xticks(fontsize=12)
 801        plt.yticks(fontsize=12)
 802        plt.xlim(left=0)
 803        plt.ylim(bottom=0)
 804        plt.title('Ships in channel', fontsize=15)
 805        plt.xlabel('Time (hr)', fontsize=15)
 806        plt.ylabel('Number of ships', fontsize=15)
 807        plt.grid(True)
 808        plt.tight_layout()
 809        plt.savefig(f'.{run_id}/plots/channel_utilization.pdf')
 810        plt.close()
 811
 812    report_path = f".{run_id}/logs/final_report.txt"
 813    with open(report_path, 'a') as f:
 814        f.write(
 815            f"Mean number of vessels in channel: {mean_total_ships_in_channel}\n\n")
 816        f.write("Mean number of vessel in each section:\n")
 817        for section, mean_ships in mean_ships_per_time_step.items():
 818            f.write(f"Section {section}: {mean_ships}\n")
 819        f.write("\n")
 820
 821    # Calculate entry and exit times for each ship
 822    file_path = f'.{run_id}/logs/channel_section_entry_exit_times.csv'
 823    ships_data = pd.read_csv(file_path)
 824
 825    # Identify the first and last sections for each ship
 826    first_section = ships_data[ships_data['section'] == 0]
 827    last_sections = ships_data.groupby(['ship', 'direction'])[
 828        'section'].max().reset_index()
 829
 830    # Merge last sections with the original dataset to get exit times
 831    last_section_exit = ships_data.merge(
 832        last_sections,
 833        on=['ship', 'section', 'direction'],
 834        how='inner'
 835    )
 836
 837    # Get entry and exit times for each ship
 838    entry_exit_times = pd.merge(
 839        first_section[['ship', 'direction', 'start']].rename(
 840            columns={'start': 'entry_time_section_0'}),
 841        last_section_exit[['ship', 'direction', 'end']].rename(
 842            columns={'end': 'exit_time_last_section'}),
 843        on=['ship', 'direction'],
 844        how='outer'
 845    )
 846
 847    # Save the entry and exit times to csv
 848    entry_exit_times.to_csv(
 849        f'.{run_id}/logs/entry_exit_times.csv', index=False)
 850
 851
 852def ships_in_anchorage_analysis(ship_type, run_id, plot=True):
 853    """
 854    Analyzes the ships in anchorage by reading the ship data from a CSV file, calculating the cumulative arrivals and channel entries, and generating a time series of ships waiting in anchorage.
 855    Args:
 856        ship_type (str): Type of ships to analyze ('all', 'Container', 'Liquid', 'DryBulk').
 857        run_id (str): Unique identifier for the run to save the analysis results.
 858        plot (bool): Whether to generate and save plots of the anchorage queue.
 859    Returns:
 860        None: Saves the analysis results to CSV files and generates plots if `plot` is True.
 861    """
 862
 863    # time series of ships that entered anchorage
 864    input_ship_data = pd.read_csv(f'.{run_id}/logs/ship_data.csv')
 865
 866    if ship_type == 'all':
 867        input_ship_data = input_ship_data
 868    else:
 869        input_ship_data = input_ship_data[input_ship_data['ship_type'] == ship_type]
 870
 871    arrival_times = input_ship_data['arrival'].dropna(
 872    ).sort_values().reset_index(drop=True)
 873    cumulative_arrivals = pd.DataFrame(
 874        {'time_index': range(1, int(arrival_times.max()) + 1)})
 875    cumulative_arrivals['cumulative_arrivals'] = cumulative_arrivals['time_index'].apply(
 876        lambda x: (arrival_times <= x).sum()
 877    )
 878
 879    # save the cumulative arrivals to csv
 880    cumulative_arrivals.to_csv(f'.{run_id}/logs/cumulative_arrivals.csv')
 881
 882    # time series of ships that entered channel
 883    file_path = f'.{run_id}/logs/channel_section_entry_exit_times.csv'
 884    ships_data = pd.read_csv(file_path)
 885
 886    if ship_type != 'all':
 887        ships_data = ships_data[ships_data['ship_type'] == ship_type]
 888
 889    ships_data = ships_data[ships_data['direction'] == 'in']
 890    ships_data = ships_data.groupby('ship').first().reset_index()
 891    channel_enter_times = ships_data['start'].reset_index(drop=True)
 892    cumulative_channel_enter = pd.DataFrame(
 893        {'time_index': range(1, int(constants.SIMULATION_TIME) + 1)})
 894    cumulative_channel_enter['cumulative_channel_enter'] = cumulative_channel_enter['time_index'].apply(
 895        lambda x: (channel_enter_times <= x).sum()
 896    )
 897
 898    # combine the two dfs
 899    combined = cumulative_arrivals.merge(
 900        cumulative_channel_enter, on='time_index', how='outer')
 901    combined['waiting_in_anchorage'] = combined['cumulative_arrivals'] - \
 902        combined['cumulative_channel_enter']
 903
 904    # svae the combined dataframe to csv
 905    combined.to_csv(f'.{run_id}/logs/waiting_in_anchorage_{ship_type}.csv')
 906
 907    # Calculate the total number of rows
 908    rows_in_combined = len(combined)
 909    mean_waiting_in_anchorage = combined.loc[constants.WARMUP_ITERS:]['waiting_in_anchorage'].mean(
 910    )
 911
 912    # plot time series of waiting in anchorage
 913    if plot:
 914        plt.plot(combined['time_index'], combined['waiting_in_anchorage'])
 915        plt.title(f'Anchorage queue - {ship_type} ships', fontsize=15)
 916        plt.xticks(fontsize=12)
 917        plt.yticks(fontsize=12)
 918        plt.xlim(left=0)
 919        plt.ylim(bottom=0)
 920        plt.xlabel('Time (hr)', fontsize=15)
 921        plt.ylabel('Number of ships', fontsize=15)
 922        plt.grid(True)
 923        plt.tight_layout()
 924    plt.savefig(f'.{run_id}/plots/anchorage_queue_{ship_type}.pdf')
 925
 926    with open(f'.{run_id}/logs/final_report.txt', 'a') as f:
 927        f.write(
 928            f"Mean number of {ship_type} ships waiting in anchorage: {mean_waiting_in_anchorage}\n\n")
 929
 930    plt.close()
 931
 932    return
 933
 934######################
 935# Trucks 
 936######################
 937
 938
 939def create_truck_csv(run_id):
 940    """
 941    Read truck_data.txt, parse into a DataFrame, save as CSV under ./<run_id>/truck_data.csv,
 942    and return the DataFrame.
 943    Args:
 944        run_id (str): Unique identifier for the run to save the truck data.
 945    Returns:
 946        pd.DataFrame: DataFrame containing the truck data with columns for truck ID, start time, dwell time, terminal ID, terminal type, and arrival time.
 947    """
 948    txt_path = f'.{run_id}/logs/truck_data.txt'
 949    data = []
 950
 951    # only take first 100000 units of data
 952    count = 0
 953    with open(txt_path, 'r') as f:
 954        for line in f:
 955            count += 1
 956            if count >= 100000:
 957                break
 958            line = line.strip()
 959            if not line:
 960                continue
 961            parts = [p.strip() for p in line.split(',')]
 962            row = {}
 963            for part in parts:
 964                key, val = part.split(':', 1)
 965                row[key.strip()] = val.strip()
 966            data.append(row)
 967
 968    df = pd.DataFrame(data)
 969    # convert types
 970    df = df.astype({
 971        'truck_id'     : int,
 972        'start_time'   : float,
 973        'dwell_time'   : float,
 974        'terminal_id'  : int,
 975        'terminal_type': str
 976    })
 977    # compute arrival-of-day
 978    df['arrival_time'] = df['start_time'] % 24
 979
 980    # make run directory & save CSV
 981    out_dir = f'.{run_id}/logs/'
 982    os.makedirs(out_dir, exist_ok=True)
 983    csv_path = os.path.join(out_dir, 'truck_data.csv')
 984    df.to_csv(csv_path, index=False)
 985
 986    return df
 987
 988
 989def plot_dwell_by_type(run_id):
 990    """
 991    Plots the dwell time distribution of trucks by terminal type.   
 992    Args:
 993        run_id (str): Unique identifier for the run to save the plots.
 994    Returns:
 995        None: Saves the plots as PDF files in the specified directories.
 996    """
 997    out_dir = f'.{run_id}/plots/TruckDwellByCargo'
 998    df = pd.read_csv(f'.{run_id}/logs/truck_data.csv')
 999    os.makedirs(out_dir, exist_ok=True)
1000
1001    for ttype, grp in df.groupby('terminal_type'):
1002        plt.figure()
1003        grp['dwell_time'].hist(bins=30)
1004        plt.title(f'Dwell Time Distribution — {ttype}')
1005        plt.xlabel('Dwell Time')
1006        plt.ylabel('Frequency')
1007        # make the y ticks as percentage
1008        plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x/len(grp)*100:.1f}%'))
1009        plt.xticks(rotation=45)
1010        plt.grid(axis='y', linestyle='--', alpha=0.7)
1011        # save the plot
1012        plt.tight_layout()
1013        fname = f'dwell_distribution_{ttype}.pdf'
1014        plt.savefig(os.path.join(out_dir, fname))
1015        # plt.show()
1016        plt.close()
1017
1018
1019def plot_dwell_by_terminal(run_id):
1020    """
1021    Plots the dwell time distribution of trucks by terminal type and terminal ID.
1022    Args:
1023        run_id (str): Unique identifier for the run to save the plots.          
1024    Returns:
1025        None: Saves the plots as PDF files in the specified directories.
1026    """
1027    df = pd.read_csv(f'.{run_id}/logs/truck_data.csv')
1028    dir_term  = f'.{run_id}/plots/TruckDwellByTerminal'
1029    os.makedirs(dir_term,  exist_ok=True)
1030
1031    for (ttype, tid), grp in df.groupby(['terminal_type','terminal_id']):
1032        plt.figure()
1033        grp['dwell_time'].hist(bins=30)
1034        plt.title(f'Dwell Time — {ttype} (Terminal {tid})')
1035        plt.xlabel('Dwell Time')
1036        plt.ylabel('Frequency')
1037        # make the y ticks as percentage
1038        plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x/len(grp)*100:.1f}%'))
1039        plt.xticks(rotation=45)
1040        plt.grid(axis='y', linestyle='--', alpha=0.7)
1041        plt.tight_layout()
1042        fname = f'dwell_distribution_{ttype}_{tid}.pdf'
1043        plt.savefig(os.path.join(dir_term,  fname))
1044        plt.close()
1045
1046
1047def plot_arrival_distributions(run_id):
1048    """
1049    Plots the arrival time distribution of trucks by terminal type and terminal ID.
1050    Args:
1051        run_id (str): Unique identifier for the run to save the plots.
1052    Returns:
1053        None: Saves the plots as JPEG files in the specified directories.   
1054    """
1055    df = pd.read_csv(f'.{run_id}/logs/truck_data.csv')
1056    dir_type  = f'.{run_id}/plots/TruckArrivalByCargo'
1057    dir_cargo = f'.{run_id}/plots/TruckArrivalByTerminal'
1058    os.makedirs(dir_type,  exist_ok=True)
1059    os.makedirs(dir_cargo, exist_ok=True)
1060
1061    # by type
1062    for ttype, grp in df.groupby('terminal_type'):
1063        plt.figure()
1064        grp['arrival_time'].hist(bins=24, range=(0,24))
1065        plt.title(f'Arrival Time Distribution — {ttype}')
1066        plt.xlabel('Hour of Day')
1067        plt.ylabel('Frequency')
1068        # make the y ticks as percentage
1069        plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x/len(grp)*100:.1f}%'))
1070        plt.xticks(rotation=45)
1071        plt.grid(axis='y', linestyle='--', alpha=0.7)
1072        plt.tight_layout()
1073        fname = f'arrival_distribution_{ttype}.jpg'
1074        plt.savefig(os.path.join(dir_type, fname))
1075        plt.close()
1076
1077    # by terminal
1078    for (ttype, tid), grp in df.groupby(['terminal_type','terminal_id']):
1079        plt.figure()
1080        grp['arrival_time'].hist(bins=24, range=(0,24))
1081        plt.title(f'Arrival Time — {ttype} (Terminal {tid})')
1082        plt.xlabel('Hour of Day')
1083        plt.ylabel('Frequency')
1084        # make the y ticks as percentage
1085        plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x/len(grp)*100:.1f}%'))
1086        plt.xticks(rotation=45)
1087        plt.grid(axis='y', linestyle='--', alpha=0.7)
1088        plt.tight_layout()
1089        fname = f'arrival_distribution_{ttype}_{tid}.jpg'
1090        plt.savefig(os.path.join(dir_cargo, fname))
1091        plt.close()
1092
1093
1094def delete_truck_data_csv(run_id):
1095    """
1096    Delete the truck_data.csv file if it exists and save the truck data as a pickle file.
1097    Args:
1098        run_id (str): Unique identifier for the run to save the truck data as a pickle file.
1099    Returns:
1100        None: Deletes the truck_data.csv file and saves the truck data as a pickle file.
1101    """
1102    csv_path = f'.{run_id}/logs/truck_data.csv'
1103    pkl_path = f'.{run_id}/logs/truck_data.pkl'
1104    df = pd.read_csv(csv_path)
1105    df.to_pickle(pkl_path)
1106
1107
1108
1109
1110######################
1111# Logs and Plots
1112#######################
1113
1114def update_ship_logs(ship_logs, ship_type, ship_id, selected_terminal, ship_start_time="nan", time_to_get_berth="nan", time_for_restriction_in="nan", time_to_get_pilot_in="nan",
1115                     time_to_get_tugs_in="nan", time_to_common_channel_in="nan", time_to_travel_channel_in="nan", time_to_tug_steer_in="nan", unloading_time="nan", loading_time="nan", waiting_time="nan",
1116                     departure_time="nan", time_to_get_pilot_out="nan", time_to_get_tugs_out="nan", time_to_tug_steer_out="nan", time_for_restriction_out='nan', time_for_uturn="nan",
1117                     time_to_travel_channel_out="nan", time_to_common_channel_out="nan", ship_end_time="nan"):
1118    """
1119    Updates the ship logs with the provided information. If a log with the same Ship ID exists, it updates the existing log; otherwise, it appends a new log.
1120    Note 1: The ship type is represented by a single character (e.g., "C" for Container, "L" for Liquid, "D" for Dry Bulk).
1121    Note 2: If the log already exists, it updates the existing log with the new values, ignoring 'nan' values.
1122    Args:
1123        ship_logs (list): List of existing ship logs.
1124        ship_type (str): Type of the ship (e.g., "C" for Container, "L" for Liquid, "D" for Dry Bulk).
1125        ship_id (int): Unique identifier for the ship.
1126        selected_terminal (str): The terminal where the ship is directed.
1127        ship_start_time (str): Start time of the ship's operation.
1128        time_to_get_berth (str): Time taken to get to the berth.
1129        time_for_restriction_in (str): Time for restriction in.
1130        time_to_get_pilot_in (str): Time taken to get the pilot in.
1131        time_to_get_tugs_in (str): Time taken to get tugs in.
1132        time_to_common_channel_in (str): Time taken to get to the common channel in.
1133        time_to_travel_channel_in (str): Time taken to travel the channel in.
1134        time_to_tug_steer_in (str): Time taken for tug steering in.
1135        unloading_time (str): Time taken for unloading.
1136        loading_time (str): Time taken for loading.
1137        waiting_time (str): Time spent waiting.
1138        departure_time (str): Departure time of the ship.
1139        time_to_get_pilot_out (str): Time taken to get the pilot out.
1140        time_to_get_tugs_out (str): Time taken to get tugs out.
1141        time_to_tug_steer_out (str): Time taken for tug steering out.
1142        time_for_restriction_out (str): Time for restriction out.
1143        time_for_uturn (str): Time taken for U-turn.
1144        time_to_travel_channel_out (str): Time taken to travel the channel out.
1145        time_to_common_channel_out (str): Time taken to get to the common channel out.
1146        ship_end_time (str): End time of the ship's operation.
1147    Returns:
1148        list: Updated ship logs with the new or modified log entry.
1149    """
1150
1151    if ship_type == "C":
1152        ship_type_full = "Container"
1153    elif ship_type == "L":
1154        ship_type_full = "Liquid"
1155    elif ship_type == "D":
1156        ship_type_full = "Dry Bulk"
1157
1158    # Create the log entry
1159    new_log = [
1160        f"Ship_{ship_id}",
1161        ship_type_full,
1162        f"{ship_type}.{selected_terminal}",
1163        ship_start_time,
1164        time_to_get_berth,
1165        time_for_restriction_in,
1166        time_to_get_pilot_in,
1167        time_to_get_tugs_in,
1168        time_to_common_channel_in,
1169        time_to_travel_channel_in,
1170        time_to_tug_steer_in,
1171        unloading_time,
1172        loading_time,
1173        waiting_time,
1174        departure_time,
1175        time_to_get_pilot_out,
1176        time_to_get_tugs_out,
1177        time_for_uturn,
1178        time_to_tug_steer_out,
1179        time_for_restriction_out,
1180        time_to_travel_channel_out,
1181        time_to_common_channel_out,
1182        None,
1183        None,
1184        ship_end_time
1185    ]
1186
1187    # Check if the log with the same Ship ID exists
1188    for i, log in enumerate(ship_logs):
1189        if log[0] == f"Ship_{ship_id}":
1190            for j, vals in enumerate(new_log):
1191                if new_log[j] != 'nan':
1192                    ship_logs[i][j] = vals
1193
1194            return ship_logs
1195
1196    # If no existing log is found, append the new log
1197    ship_logs.append(new_log)
1198
1199    return ship_logs
1200
1201
1202def analyse_bays_chassis_utilization(chassis_bays_utilization, num_terminals_list, run_id):
1203    """
1204    Generates plots for chassis and bay utilization at each terminal type.
1205    Args:
1206        chassis_bays_utilization (dict): Dictionary containing chassis and bay utilization data for each terminal type.
1207        num_terminals_list (list): List containing the number of terminals for each terminal type.
1208        run_id (str): Unique identifier for the run to save the plots.
1209    Returns:
1210        None: Saves the plots as PDF files in the specified directory.
1211    """
1212    for terminal_type in ["Container", "Liquid", "DryBulk"]:
1213        for terminal_id in range(1, num_terminals_list[["Container", "Liquid", "DryBulk"].index(terminal_type)] + 1):
1214            to_plot = chassis_bays_utilization[terminal_type][terminal_id]
1215
1216            x_vals = []
1217            y_vals = []
1218
1219            for vals in to_plot:
1220                x_vals.append(vals[0])
1221                y_vals.append(vals[1])
1222
1223            plt.figure(figsize=(12, 8))
1224            plt.plot(x_vals, y_vals)
1225            plt.title(
1226                f'Chassis / Bay Utilization at {terminal_type} Terminal {terminal_id}')
1227            plt.xlabel('Time')
1228            plt.ylabel('Utilization')
1229            plt.savefig(
1230                f'.{run_id}/bottlenecks/chassisBays/{terminal_type}_Terminal_{terminal_id}.pdf')
1231            plt.close()
1232
1233
1234def gen_logs_and_plots(run_id, ship_logs, events, chassis_bays_utilization, num_terminals_list, train_events, channel_logs, channel_events, channel, animate=True):
1235    """
1236    Generates logs and plots from the provided ship logs, events, chassis and bay utilization data, train events, and channel logs.
1237    Saves the logs to an Excel file and generates various plots including process charts, dwell times, turn times, truck dwell times, and channel utilization.
1238    Also generates utilization reports for resources such as berths and yards.
1239    The following plots are generated:
1240        - Process chart for ship operations
1241        - Dwell time chart for ships
1242        - Turn time analysis for ships
1243        - Truck dwell times and arrival times at terminals
1244        - Channel utilization over time
1245        - Chassis and bay utilization at each terminal type
1246    The results are saved in the specified run directory.
1247    Args:
1248        run_id (str): Unique identifier for the run to save the logs and plots.
1249        ship_logs (list): List of ship logs containing information about each ship's operations.
1250        events (list): List of events where each event is a tuple containing:
1251                          (event_type, time, name, terminal_id, terminal_type)
1252        chassis_bays_utilization (dict): Dictionary containing chassis and bay utilization data for each terminal type.
1253        num_terminals_list (list): List containing the number of terminals for each terminal type.
1254        train_events (dict): Dictionary containing train events where keys are event names and values are lists of event data.
1255        channel_logs (list): List of channel logs containing information about the channel's operations.
1256        channel_events (list): List of channel events where each event is a tuple containing:
1257                          (event_type, time, name, section_id, section_type)
1258        channel (object): Channel object containing sections and their respective containers.
1259    Returns:
1260        None: Saves the logs and plots in the specified directory.
1261    """
1262    def extract_t(restriction_str):
1263        if isinstance(restriction_str, str):
1264            parts = restriction_str.split(", ")
1265            for part in parts:
1266                if part.startswith("T"):
1267                    return float(part.split(":")[1])
1268        return 0.0
1269
1270    # Ship logs
1271    columns = ["Ship_Id", "Terminal Type", "Terminal Directed", "Start Time", "Time to get Berth", "Time for Restriction In", "Time to get Pilot In", "Time to get Tugs In", "Time to Common Channel In", "Time to Travel Channel In", "Time to Tug Steer In", "Unloading Time",
1272               "Loading Time", "Waiting Time", "Departure Time", "Time to get Pilot Out", "Time to get Tugs Out", "Time for U-Turn", "Time to Tug Steer Out", "Time for Restriction Out", "Time to Travel Channel Out", "Time to Common Channel Out", "Turn Time", "Dwell Time", "End Time"]
1273    df = pd.DataFrame(ship_logs, columns=columns)
1274
1275    # all columns in float format of 2 decimal places
1276    for col in df.columns:
1277        if col not in ["Ship_Id", "Terminal Type", "Terminal Directed", "Time for Restriction In", "Time for Restriction Out"]:
1278            df[col] = df[col].astype(float).round(2)
1279
1280    df['Ship_Id'] = df['Ship_Id'].str.extract(r'(\d+)').astype(int)
1281    df = df.sort_values(by='Ship_Id')
1282    df['Ship_Id'] = "Ship_" + df['Ship_Id'].astype(str)
1283    df['Turn Time'] = df['End Time'] - df['Start Time']
1284    df['Dwell Time'] = df['Unloading Time'] + \
1285        df['Loading Time'] + df['Waiting Time']
1286
1287    df['Restriction In (T)'] = df['Time for Restriction In'].apply(extract_t)
1288    df['Anchorage Time'] = df['Time to get Berth'] + \
1289        df['Restriction In (T)'] + df['Time to get Pilot In'] + \
1290        df['Time to get Tugs In']
1291    # if ship type is container add anchorage waiting_container as a column
1292    for index, row in df.iterrows():
1293        if row['Terminal Type'] == 'Container':
1294            df.at[index, 'Anchorage Time'] += constants.ANCHORAGE_WAITING_CONTAINER
1295        elif row['Terminal Type'] == 'Liquid':
1296            df.at[index, 'Anchorage Time'] += constants.ANCHORAGE_WAITING_LIQUID
1297        elif row['Terminal Type'] == 'Dry Bulk':
1298            df.at[index, 'Anchorage Time'] += constants.ANCHORAGE_WAITING_DRYBULK
1299
1300    df.to_excel(f'.{run_id}/logs/ship_logs.xlsx', index=False)
1301
1302    warmup_period = constants.WARMUP_ITERS / len(df)
1303    mean_anchorage_time = df['Anchorage Time'].sort_values(
1304    ).iloc[constants.WARMUP_ITERS:].mean()
1305    mean_anchorage_time_ctr = df[df['Terminal Type'] == 'Container']['Anchorage Time'].sort_values(
1306    ).iloc[int(warmup_period * len(df[df['Terminal Type'] == 'Container'])):].mean()
1307    mean_anchorage_time_liq = df[df['Terminal Type'] == 'Liquid']['Anchorage Time'].sort_values(
1308    ).iloc[int(warmup_period * len(df[df['Terminal Type'] == 'Liquid'])):].mean()
1309    mean_anchorage_time_db = df[df['Terminal Type'] == 'Dry Bulk']['Anchorage Time'].sort_values(
1310    ).iloc[int(warmup_period * len(df[df['Terminal Type'] == 'Dry Bulk'])):].mean()
1311
1312    report_path = f".{run_id}/logs/final_report.txt"
1313    with open(report_path, 'a') as f:
1314        f.write(f"Mean Anchorage Time: {mean_anchorage_time}\n")
1315        f.write(f"Mean Anchorage Time Container: {mean_anchorage_time_ctr}\n")
1316        f.write(f"Mean Anchorage Time Liquid: {mean_anchorage_time_liq}\n")
1317        f.write(f"Mean Anchorage Time Dry Bulk: {mean_anchorage_time_db}\n\n")
1318
1319    # Generate the process chart
1320    plot_process_chart(events, run_id=run_id)
1321
1322    # Generate the dwell time chart
1323    plot_dwell_times(events, run_id)
1324    analyze_turn_time_ships(run_id)
1325    get_dists(run_id)
1326
1327    # Plot queueus in channel
1328    ships_in_channel_analysis(run_id)
1329    ships_in_anchorage_analysis(ship_type='all', run_id=run_id)
1330    ships_in_anchorage_analysis(ship_type='Container', run_id=run_id)
1331    ships_in_anchorage_analysis(ship_type='DryBulk', run_id=run_id)
1332    ships_in_anchorage_analysis(ship_type='Liquid', run_id=run_id)
1333
1334    # Trucks
1335    create_truck_csv(run_id)
1336    plot_dwell_by_type(run_id)
1337    plot_dwell_by_terminal(run_id)
1338    plot_arrival_distributions(run_id)
1339    delete_truck_data_csv(run_id)
1340
1341    # Bays & Chassis Utilization
1342    analyse_bays_chassis_utilization(
1343        chassis_bays_utilization, num_terminals_list, run_id)
1344
1345    # Train
1346    gen_train_df(train_events, run_id)
1347
1348    # tugs and pilots
1349    tug_pilot_df = pd.read_csv(f'.{run_id}/logs/pilots_tugs_data.csv')
1350
1351    # plot day anf night pilots
1352    plt.figure(figsize=(12, 8))
1353    plt.plot(tug_pilot_df['Time'],
1354             tug_pilot_df['Day Pilots'], label='Day Pilots')
1355    plt.title('Number of day pilots available')
1356    plt.xlabel('Time')
1357    plt.ylabel('Number of pilots')
1358    plt.ylim(0, constants.NUM_PILOTS_DAY[1])
1359    plt.savefig(f'.{run_id}/plots/DayPilots.pdf')
1360    plt.close()
1361
1362    plt.figure(figsize=(12, 8))
1363    plt.plot(tug_pilot_df['Time'],
1364             tug_pilot_df['Night Pilots'], label='Night Pilots')
1365    plt.title('Number of night pilots available')
1366    plt.xlabel('Time')
1367    plt.ylabel('Number of pilots')
1368    plt.ylim(0, constants.NUM_PILOTS_NIGHT[1])
1369    plt.savefig(f'.{run_id}/plots/NightPilots.pdf')
1370    plt.close()
1371
1372    # plot tugboats
1373    plt.figure(figsize=(12, 8))
1374    plt.plot(tug_pilot_df['Time'], tug_pilot_df['Tugboats'], label='Tugboats')
1375    plt.title('Tugboats')
1376    plt.xlabel('Time')
1377    plt.ylabel('Number of tugboats available')
1378    plt.ylim(0, constants.NUM_TUGBOATS[1])
1379    plt.legend()
1380    plt.savefig(f'.{run_id}/plots/Tugboats.pdf')
1381    plt.close()
1382
1383    # Animate the channel usage
1384    if animate:
1385        animate_channel_usage(run_id, channel)
1386
1387    # print("Done!")
1388
1389
1390#######################
1391# Channel Utilization Animation (DEPRECATED)
1392########################
1393
1394def animate_channel_usage(run_id, channel):
1395    """
1396    DEPRICIATED
1397    Animates the channel usage over time, showing width and draft utilization.
1398    Args:
1399        run_id (str): Unique identifier for the run to save the animation.
1400        channel (object): Channel object containing sections and their respective containers.
1401    Returns:
1402        None: Saves the animation as a GIF file in the specified directory.
1403    """
1404    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
1405
1406    channel_sections = constants.CHANNEL_SECTION_DIMENSIONS
1407
1408    x_max = len(channel.sections)
1409    y_max = 100
1410
1411    def init():
1412
1413        for i in range(1, len(channel.sections)+1):
1414            ax1.bar(i,  100 * channel.sections[i-1].width_containers[0].level /
1415                    channel.sections[i-1].width_containers[0].capacity, color='b')
1416            ax2.bar(i, 100 * channel.sections[i-1].draft_containers[0].level /
1417                    channel.sections[i-1].draft_containers[0].capacity, color='r')
1418        ax1.set_xlim(0, x_max)
1419        ax1.set_ylim(0, y_max)
1420        ax2.set_xlim(0, x_max)
1421        ax2.set_ylim(0, y_max)
1422        ax1.set_title('Width utilization')
1423        ax2.set_title('Draft utilization')
1424        return ax1.patches + ax2.patches
1425
1426    progress_bar = tqdm(total=int(len(channel.sections[0].width_containers)/2))
1427
1428    def animate(t):
1429
1430        ax1.clear()
1431        ax2.clear()
1432
1433        # print(f'Animating {t}')
1434        progress_bar.update(1)
1435        for i in range(1, len(channel.sections) + 1):
1436            ax1.bar(i,  100 * channel.sections[i-1].width_containers[t].level /
1437                    channel.sections[i-1].width_containers[t].capacity,  color='b')
1438            ax2.bar(i, 100 * channel.sections[i-1].draft_containers[t].level /
1439                    channel.sections[i-1].draft_containers[t].capacity, color='r')
1440            # print(t, i, channel.sections[i-1].width_containers[0].level, channel.sections[i-1].width_containers[0].capacity)
1441
1442        ax1.set_xlim(0, x_max)
1443        ax1.set_ylim(0, y_max)
1444        ax2.set_xlim(0, x_max)
1445        ax2.set_ylim(0, y_max)
1446        fig.suptitle(f'Time Step: {t}', fontsize=16, fontweight='bold')
1447        return ax1.patches + ax2.patches
1448
1449    ani = animation.FuncAnimation(fig, animate, init_func=init, frames=range(
1450        int(len(channel.sections[0].width_containers)/2)), interval=1, blit=False)
1451
1452    # Save the animation as gif
1453    ani.save(f'.{run_id}/animations/channel_avilability.gif',
1454             writer='pillow', fps=1000)
1455    plt.close()
def plot_process_chart(events, run_id):
28def plot_process_chart(events, run_id):
29    """
30    # TODO: Ensure this is compatible with the new event structure.
31    Generates process charts for each terminal based on the events.
32    Args:
33        events (list): List of events where each event is a tuple containing:
34                       (event_name, terminal_type, terminal_id, event_type, time, additional_info)
35        run_id (str): Unique identifier for the run to save the plots.
36    Returns:
37        None: Saves process charts as images in the specified directory.
38    """
39    terminal_events = {}
40
41    output_dir = f'.{run_id}/plots/TerminalProcessCharts'
42    if not os.path.exists(output_dir):
43        os.makedirs(output_dir)
44
45    for event in events:
46        try:
47            _, terminal_type, terminal_id, event_type, time, _ = event
48            key = (terminal_type, terminal_id)
49            if key not in terminal_events:
50                terminal_events[key] = {}
51            terminal_events[key].setdefault(event_type, []).append(time)
52        except:
53            if event[0] != "Truck Arrival" and event[0] != "Truck Departure":
54                print(event)
55            pass
56
57    for (terminal_type, terminal_id), events in terminal_events.items():
58        try:
59            df = pd.DataFrame()
60            df['Time'] = events['arrive'] + events['dock'] + \
61                sum([events[e] for e in events if e not in [
62                    'arrive', 'dock', 'depart']], []) + events['depart']
63            df['Event'] = ['Ship Arrival'] * len(events['arrive']) + ['Ship Dock'] * len(events['dock']) + sum([[e] * len(
64                events[e]) for e in events if e not in ['arrive', 'dock', 'depart']], []) + ['Ship Depart'] * len(events['depart'])
65
66            plt.figure(figsize=(10, 6))
67            plt.scatter(df['Time'], df['Event'])
68            plt.title(
69                f'Process Chart for {terminal_type} Terminal {terminal_id}')
70            plt.xlabel('Time')
71            plt.ylabel('Event')
72
73            plt.savefig(
74                f'{output_dir}/process_chart_{terminal_type}_{terminal_id}.jpg')
75            plt.close()
76        except KeyError:
77            pass

TODO: Ensure this is compatible with the new event structure.

Generates process charts for each terminal based on the events.

Arguments:
  • events (list): List of events where each event is a tuple containing: (event_name, terminal_type, terminal_id, event_type, time, additional_info)
  • run_id (str): Unique identifier for the run to save the plots.
Returns:

None: Saves process charts as images in the specified directory.

def plot_dwell_times(events, run_id):
 84def plot_dwell_times(events, run_id):
 85    """
 86    Generates plots for dwell times and turn times of ships at terminals.
 87    Args:
 88        events (list): List of events where each event is a tuple containing:
 89                       (ship_name, terminal_type, terminal_id, event_type, time, additional_info)
 90        run_id (str): Unique identifier for the run to save the plots.
 91    Returns:
 92        None: Saves plots as images in the specified directory.
 93    """
 94
 95    all_dwell_times = []
 96    all_turn_times = []
 97
 98    ship_arrival_times = []
 99    ship_dock_times = []
100    ship_undock_times = []
101    ship_departure_times = []
102
103    dwell_times = {}
104    turn_times = {}
105
106    # read the events
107    for event in events:
108        try:
109            name, terminal_type, terminal_id, event_type, time, _ = event
110            if event_type == "arrive":  # arrive at anchorage
111                ship_arrival_times.append(
112                    (name, terminal_type, terminal_id, time))
113            if event_type == "dock":
114                ship_dock_times.append(
115                    (name, terminal_type, terminal_id, time))
116            if event_type == "undock":
117                ship_undock_times.append(
118                    (name, terminal_type, terminal_id, time))
119            elif event_type == "depart":
120                ship_departure_times.append(
121                    (name, terminal_type, terminal_id, time))
122        except:
123            pass
124
125    # Calculate turn times
126    for ship, terminal_type, terminal_id, arrival_time in ship_arrival_times:
127        for name, t_type, t_id, departure_time in ship_departure_times:
128            if ship == name and terminal_type == t_type and terminal_id == t_id:
129                key = (terminal_type, terminal_id)
130                if key not in turn_times:
131                    turn_times[key] = {}
132                turn_times[key][ship] = departure_time - arrival_time
133                all_turn_times.append(departure_time - arrival_time)
134
135    # Calculate dwell times
136    for ship, terminal_type, terminal_id, dock_time in ship_dock_times:
137        for name, t_type, t_id, undock_time in ship_undock_times:
138            if ship == name and terminal_type == t_type and terminal_id == t_id:
139                key = (terminal_type, terminal_id)
140                if key not in dwell_times:
141                    dwell_times[key] = {}
142                dwell_times[key][ship] = undock_time - dock_time
143                all_dwell_times.append(undock_time - dock_time)
144
145    # Create bar plot of the dwell times for individual vessels, by unique terminal
146    for (terminal_type, terminal_id), ships_dwell_times in dwell_times.items():
147        try:
148            df = pd.DataFrame(ships_dwell_times.items(),
149                              columns=['Ship', 'Dwell Time'])
150            # Set 'Ship' as index for proper labeling on x-axis
151            df.set_index('Ship', inplace=True)
152            df.plot(kind='bar', y='Dwell Time', legend=False,
153                    title=f'Dwell times of ships at {terminal_type} terminal {terminal_id}')
154            plt.ylabel('Dwell time (hr)')
155            plt.savefig(
156                f'.{run_id}/plots/DwellTimes/dwell_times_{terminal_type}_{terminal_id}.jpg')
157            plt.close()  # Clear the figure after saving the plot
158        except Exception as e:
159            print(
160                f"Error in dwell time plot generation for {terminal_type} terminal {terminal_id}: {e}")
161            pass
162
163    # Create bar plot of the turn times for individual vessels, by unique terminal
164    for (terminal_type, terminal_id), ships_turn_times in turn_times.items():
165        try:
166            df = pd.DataFrame(ships_turn_times.items(),
167                              columns=['Ship', 'Turn Time'])
168            # Set 'Ship' as index for proper labeling on x-axis
169            df.set_index('Ship', inplace=True)
170            df.plot(kind='bar', y='Turn Time', legend=False,
171                    title=f'Turn Times of Ships at {terminal_type} Terminal {terminal_id}')
172            plt.ylabel('Turn Time')
173            plt.savefig(
174                f'.{run_id}/plots/TurnTimes/turn_times_{terminal_type}_{terminal_id}.jpg')
175            plt.close()  # Clear the figure after saving the plot
176        except Exception as e:
177            print(
178                f"Error in turn time plot generation for {terminal_type} Terminal {terminal_id}: {e}")
179            pass
180
181    # Create histogram of the dwell times of all vessels visiting each unique terminal
182    for (terminal_type, terminal_id), ships_dwell_times in dwell_times.items():
183        try:
184            dwell_times_list = list(ships_dwell_times.values())
185            plt.figure(figsize=(10, 6))
186            plt.hist(dwell_times_list, bins=20, edgecolor='black')
187            plt.title(
188                f'Dwell time distribution at {terminal_type} terminal {terminal_id}')
189            plt.xlabel('Dwell time (hr)')
190            plt.ylabel('Frequency')
191            plt.savefig(
192                f'.{run_id}/plots/DwellTimesDist/dwell_distribution_{terminal_type}_{terminal_id}.jpg')
193            plt.close()
194        except Exception as e:
195            print(
196                f"Error in dwell time distribution plot generation for {terminal_type} terminal {terminal_id}: {e}")
197            pass
198
199    # Create histogram of the turn times of all vessels visiting each unique terminal
200    for (terminal_type, terminal_id), ships_turn_times in turn_times.items():
201        try:
202            turn_times_list = list(ships_turn_times.values())
203            plt.figure(figsize=(10, 6))
204            plt.hist(turn_times_list, bins=20, edgecolor='black')
205            plt.title(
206                f'Turn time distribution at {terminal_type} terminal {terminal_id}', fontsize=20)
207            plt.xlabel('Turn time (hr)', fontsize=16)
208            plt.ylabel('Frequency', fontsize=16)
209            plt.savefig(
210                f'.{run_id}/plots/TurnTimesDist/turn_distribution_{terminal_type}_{terminal_id}.jpg')
211            plt.close()
212        except Exception as e:
213            print(
214                f"Error in turn time distribution plot generation for {terminal_type} terminal {terminal_id}: {e}")
215            pass
216
217    # Create histogram of dwell times for all vessels in port system (all terminals)
218    all_dwell_times = [time for terminal_dwell_times in dwell_times.values(
219    ) for time in terminal_dwell_times.values()]
220    plt.figure(figsize=(10, 6))
221    plt.hist(all_dwell_times, bins=20, edgecolor='black')
222    plt.title('Overall vessel terminal dwell time distribution', fontsize=20)
223    plt.xlabel('Dwell time (hr)', fontsize=16)
224    plt.ylabel('Frequency', fontsize=16)
225    plt.xticks(fontsize=12)
226    plt.yticks(fontsize=12)
227    plt.savefig(f'.{run_id}/plots/dwell_time_distribution.jpg')
228    plt.close()
229
230    # Create histogram of turn times for all vessels in port system (all terminals)
231    all_turn_times = [time for terminal_turn_times in turn_times.values()
232                      for time in terminal_turn_times.values()]
233    plt.figure(figsize=(10, 6))
234    plt.hist(all_turn_times, bins=20, edgecolor='black')
235    plt.title('Overall vessel turn time distribution', fontsize=20)
236    plt.xlabel('Turn time (hr)', fontsize=16)
237    plt.ylabel('Frequency', fontsize=16)
238    plt.xticks(fontsize=12)
239    plt.yticks(fontsize=12)
240    plt.savefig(f'.{run_id}/plots/turn_time_distribution.jpg')
241    plt.close()
242
243    report_path = f".{run_id}/logs/final_report.txt"
244    with open(report_path, 'a') as f:
245        f.write("Dwell Time Analysis:\n")
246        f.write(f"Max dwell time: {max(all_dwell_times)}\n")
247        f.write(
248            f"Median dwell time: {sorted(all_dwell_times)[len(all_dwell_times) // 2]}\n")
249        f.write(
250            f"Average dwell time: {sum(all_dwell_times) / len(all_dwell_times)}\n\n")
251
252        f.write("Turn Time Analysis:\n")
253        f.write(f"Max turn time: {max(all_turn_times)}\n")
254        f.write(
255            f"Median turn time: {sorted(all_turn_times)[len(all_turn_times) // 2]}\n")
256        f.write(
257            f"Average turn time: {sum(all_turn_times) / len(all_turn_times)}\n\n")
258
259    return None

Generates plots for dwell times and turn times of ships at terminals.

Arguments:
  • events (list): List of events where each event is a tuple containing: (ship_name, terminal_type, terminal_id, event_type, time, additional_info)
  • run_id (str): Unique identifier for the run to save the plots.
Returns:

None: Saves plots as images in the specified directory.

def analyze_turn_time_ships(run_id):
262def analyze_turn_time_ships(run_id):
263    """ Analyze the turn times and dwell times of ships from the Excel file and save the results to a report.
264    Args:
265        run_id (str): Unique identifier for the run to save the report.
266    Returns:
267        None: Saves the analysis results to a text file in the specified directory.
268    """
269    # Load the Excel file
270    file_path = f'.{run_id}/logs/ship_logs.xlsx'
271
272    df = pd.read_excel(file_path)
273    mean_dwell_times = df.groupby(['Terminal Directed'])[
274        'Turn Time'].mean().reset_index()
275    with open(f".{run_id}/logs/final_report.txt", 'a') as f:
276        f.write("Mean Turn Times by Terminal Directed:\n")
277        f.write(str(mean_dwell_times) + "\n\n")
278    mean_dwell_times = df.groupby(['Terminal Type'])[
279        'Turn Time'].mean().reset_index()
280    with open(f".{run_id}/logs/final_report.txt", 'a') as f:
281        f.write("Mean Turn Times by Terminal Type:\n")
282        f.write(str(mean_dwell_times) + "\n\n")
283
284    mean_dwell_times = df.groupby(['Terminal Directed'])[
285        'Dwell Time'].mean().reset_index()
286    with open(f".{run_id}/logs/final_report.txt", 'a') as f:
287        f.write("Mean Dwell Times by Terminal Directed:\n")
288        f.write(str(mean_dwell_times) + "\n\n")
289    mean_dwell_times = df.groupby(['Terminal Type'])[
290        'Dwell Time'].mean().reset_index()
291    with open(f".{run_id}/logs/final_report.txt", 'a') as f:
292        f.write("Mean Dwell Times by Terminal Type:\n")
293        f.write(str(mean_dwell_times) + "\n\n")

Analyze the turn times and dwell times of ships from the Excel file and save the results to a report.

Arguments:
  • run_id (str): Unique identifier for the run to save the report.
Returns:

None: Saves the analysis results to a text file in the specified directory.

def plot_queues(SHIPS_IN_CHANNEL_TRACK, name, place, run_id):
300def plot_queues(SHIPS_IN_CHANNEL_TRACK, name, place, run_id):
301    """
302    Plots the number of ships in the channel over time.
303    Args:
304        SHIPS_IN_CHANNEL_TRACK (list): List of tuples containing time and number of ships in the channel.
305        name (str): Name of the ship type (e.g., "Container", "Liquid", "Dry Bulk").
306        place (str): Place where the ships are tracked (e.g., "Port").
307        run_id (str): Unique identifier for the run to save the plot.
308    Returns:    
309        None: Saves the plot as a PDF file in the specified directory.
310    """
311
312    # Plotting
313    plt.figure(figsize=(12, 8))
314
315    x = [i[0] for i in SHIPS_IN_CHANNEL_TRACK]
316    y = [i[1] for i in SHIPS_IN_CHANNEL_TRACK]
317    plt.plot(x, y)
318    plt.suptitle(f'Number of {name} over time', fontsize=20)
319    plt.title(f'at {place}', fontsize=20)
320    plt.xlabel('Time', fontsize=15)
321    plt.ylabel(f'Number of {name}', fontsize=15)
322    plt.xticks(fontsize=12)
323    plt.yticks(fontsize=12)
324    plt.tight_layout()
325    plt.xlim(min(x)-1, max(x)+2)
326    plt.ylim(min(y)-1, max(y)+2)
327    plt.savefig(
328        f'.{run_id}/plots/{name}in{place}OverTime{constants.mean_interarrival_time_total}.pdf')
329
330    report_path = "./logs/final_report.txt"
331    with open(report_path, 'a') as f:
332        f.write(f"Mean number of {name} in {place}: {sum(y) / len(y)}\n\n")
333
334    plt.close()

Plots the number of ships in the channel over time.

Arguments:
  • SHIPS_IN_CHANNEL_TRACK (list): List of tuples containing time and number of ships in the channel.
  • name (str): Name of the ship type (e.g., "Container", "Liquid", "Dry Bulk").
  • place (str): Place where the ships are tracked (e.g., "Port").
  • run_id (str): Unique identifier for the run to save the plot.

Returns:
None: Saves the plot as a PDF file in the specified directory.

def track_utilization(container_data, liquid_data, drybulk_data, run_id):
342def track_utilization(container_data, liquid_data, drybulk_data, run_id):
343    """
344    Calculates the mean utilization of resources (berths and yards) for container, liquid, and dry bulk terminals.
345    Args:
346        container_data (pd.DataFrame): DataFrame containing container terminal data.
347        liquid_data (pd.DataFrame): DataFrame containing liquid terminal data.
348        drybulk_data (pd.DataFrame): DataFrame containing dry bulk terminal data.
349        run_id (str): Unique identifier for the run to save the results.
350    Returns:
351        list: A list containing the mean utilization of resources for each terminal type.
352    """
353
354    track_list = []
355
356    excel_data = f'.{run_id}/logs/availability.xlsx'
357
358    def calculate_mean_utilization(data, resource_type):
359        available_columns = [
360            col for col in data.columns if 'Available' in col and resource_type in col]
361        used_columns = [
362            col for col in data.columns if 'Used' in col and resource_type in col]
363
364        utilization = {}
365        for available, used in zip(available_columns, used_columns):
366            terminal_name = available.split('_Available_')[0]
367            utilization[terminal_name] = data[used].sum(
368            ) / (data[available].sum() + data[used].sum())
369
370        overall_utilization = sum(data[used_columns].sum(
371        )) / (sum(data[available_columns].sum()) + sum(data[used_columns].sum()))
372
373        return utilization, overall_utilization
374
375    container_berth_utilization, container_berth_overall = calculate_mean_utilization(
376        container_data, 'Berth_Ctr')
377    container_yard_utilization, container_yard_overall = calculate_mean_utilization(
378        container_data, 'Yard')
379
380    liquid_berth_utilization, liquid_berth_overall = calculate_mean_utilization(
381        liquid_data, 'Berth_liq')
382    liquid_tank_utilization, liquid_tank_overall = calculate_mean_utilization(
383        liquid_data, 'Tank')
384
385    drybulk_berth_utilization, drybulk_berth_overall = calculate_mean_utilization(
386        drybulk_data, 'Berth_db')
387    drybulk_silo_utilization, drybulk_silo_overall = calculate_mean_utilization(
388        drybulk_data, 'Silo')
389
390    track_list = [container_berth_utilization, container_berth_overall, container_yard_utilization, container_yard_overall, liquid_berth_utilization,
391                  liquid_berth_overall, liquid_tank_utilization, liquid_tank_overall, drybulk_berth_utilization, drybulk_berth_overall, drybulk_silo_utilization, drybulk_silo_overall]
392
393    return track_list

Calculates the mean utilization of resources (berths and yards) for container, liquid, and dry bulk terminals.

Arguments:
  • container_data (pd.DataFrame): DataFrame containing container terminal data.
  • liquid_data (pd.DataFrame): DataFrame containing liquid terminal data.
  • drybulk_data (pd.DataFrame): DataFrame containing dry bulk terminal data.
  • run_id (str): Unique identifier for the run to save the results.
Returns:

list: A list containing the mean utilization of resources for each terminal type.

def get_utilization(container_data, liquid_data, drybulk_data, run_id):
396def get_utilization(container_data, liquid_data, drybulk_data, run_id):
397    """
398    Calculates the mean utilization of resources (berths and yards) for container, liquid, and dry bulk terminals.
399    Args:
400        container_data (pd.DataFrame): DataFrame containing container terminal data.
401        liquid_data (pd.DataFrame): DataFrame containing liquid terminal data.
402        drybulk_data (pd.DataFrame): DataFrame containing dry bulk terminal data.
403        run_id (str): Unique identifier for the run to save the results.
404    Returns:
405        list: A list containing the mean utilization of resources for each terminal type.
406    """
407
408    track_list = []
409
410    excel_data = f'.{run_id}/logs/availability.xlsx'
411
412    def calculate_mean_utilization(data, resource_type):
413        available_columns = [
414            col for col in data.columns if 'Available' in col and resource_type in col]
415        used_columns = [
416            col for col in data.columns if 'Used' in col and resource_type in col]
417        # print("used:", used_columns)
418        utilization = {}
419        for available, used in zip(available_columns, used_columns):
420            terminal_name = available.split('_Available_')[0]
421            # print("data", data)
422            # print("used:", used)
423            # print(data[used])
424            # print(list(data[used]))
425            utilization[terminal_name] = list(
426                data[used])[-1] / (list(data[available])[-1] + list(data[used])[-1])
427        overall_utilization = data[used_columns].iloc[-1].sum() / (
428            data[available_columns].iloc[-1].sum() + data[used_columns].iloc[-1].sum())
429
430        return utilization, overall_utilization
431
432    container_berth_utilization, container_berth_overall = calculate_mean_utilization(
433        container_data, 'Berth_Ctr')
434    container_yard_utilization, container_yard_overall = calculate_mean_utilization(
435        container_data, 'Yard')
436
437    liquid_berth_utilization, liquid_berth_overall = calculate_mean_utilization(
438        liquid_data, 'Berth_liq')
439    liquid_tank_utilization, liquid_tank_overall = calculate_mean_utilization(
440        liquid_data, 'Tank')
441
442    drybulk_berth_utilization, drybulk_berth_overall = calculate_mean_utilization(
443        drybulk_data, 'Berth_db')
444    drybulk_silo_utilization, drybulk_silo_overall = calculate_mean_utilization(
445        drybulk_data, 'Silo')
446
447    track_list = [container_berth_utilization, container_berth_overall, container_yard_utilization, container_yard_overall, liquid_berth_utilization,
448                  liquid_berth_overall, liquid_tank_utilization, liquid_tank_overall, drybulk_berth_utilization, drybulk_berth_overall, drybulk_silo_utilization, drybulk_silo_overall]
449
450    return track_list

Calculates the mean utilization of resources (berths and yards) for container, liquid, and dry bulk terminals.

Arguments:
  • container_data (pd.DataFrame): DataFrame containing container terminal data.
  • liquid_data (pd.DataFrame): DataFrame containing liquid terminal data.
  • drybulk_data (pd.DataFrame): DataFrame containing dry bulk terminal data.
  • run_id (str): Unique identifier for the run to save the results.
Returns:

list: A list containing the mean utilization of resources for each terminal type.

def add_to_report(report_lines, resource, utilization, overall_utilization):
453def add_to_report(report_lines, resource, utilization, overall_utilization):
454    """
455    Adds the mean utilization of a resource to the report lines.
456    Args:
457        report_lines (list): List of report lines to which the utilization will be added.
458        resource (str): Name of the resource (e.g., "Container berth", "Liquid storage").
459        utilization (dict): Dictionary containing the mean utilization for each terminal.
460        overall_utilization (float): Overall mean utilization for the resource.
461    Returns:
462        None: Appends the utilization information to the report lines.
463    """
464
465    report_lines.append(f"Mean {resource} Utilization:")
466    for terminal, mean_util in utilization.items():
467        report_lines.append(f"{terminal}: {mean_util:.2%}")
468    report_lines.append(
469        f"Overall {resource} Utilization: {overall_utilization:.2%}")
470    report_lines.append("\n")

Adds the mean utilization of a resource to the report lines.

Arguments:
  • report_lines (list): List of report lines to which the utilization will be added.
  • resource (str): Name of the resource (e.g., "Container berth", "Liquid storage").
  • utilization (dict): Dictionary containing the mean utilization for each terminal.
  • overall_utilization (float): Overall mean utilization for the resource.
Returns:

None: Appends the utilization information to the report lines.

def save_track_list(run_id, timestep, track_list):
473def save_track_list(run_id, timestep, track_list):
474    """
475    Saves the utilization track list to a text file for the given timestep.
476    Args:
477        run_id (str): Unique identifier for the run to save the report.
478        timestep (int): The current timestep for which the report is generated.
479        track_list (list): List containing the mean utilization of resources for each terminal type.
480    Returns:
481        None: Saves the report to a text file in the specified directory.
482    """
483
484    container_berth_utilization, container_berth_overall, container_yard_utilization, container_yard_overall, liquid_berth_utilization, liquid_berth_overall, liquid_tank_utilization, liquid_tank_overall, drybulk_berth_utilization, drybulk_berth_overall, drybulk_silo_utilization, drybulk_silo_overall = track_list
485    report_lines = []
486
487    # Container Terminals
488    add_to_report(report_lines, "Container berth",
489                  container_berth_utilization, container_berth_overall)
490    add_to_report(report_lines, "Container storage",
491                  container_yard_utilization, container_yard_overall)
492
493    # Liquid Terminals
494    add_to_report(report_lines, "Liquid berth",
495                  liquid_berth_utilization, liquid_berth_overall)
496    add_to_report(report_lines, "Liquid storage",
497                  liquid_tank_utilization, liquid_tank_overall)
498
499    # Dry Bulk Terminals
500    add_to_report(report_lines, "Drybulk berth",
501                  drybulk_berth_utilization, drybulk_berth_overall)
502    add_to_report(report_lines, "Drybulk storage",
503                  drybulk_silo_utilization, drybulk_silo_overall)
504
505    # Save report to text file
506    report_path = f'.{run_id}/logs/availability/{timestep}.txt'
507    with open(report_path, 'a') as file:
508        file.write(f"Time Step: {timestep}\n")
509        file.write("\n".join(report_lines))

Saves the utilization track list to a text file for the given timestep.

Arguments:
  • run_id (str): Unique identifier for the run to save the report.
  • timestep (int): The current timestep for which the report is generated.
  • track_list (list): List containing the mean utilization of resources for each terminal type.
Returns:

None: Saves the report to a text file in the specified directory.

def gen_train_df(train_events, run_id):
516def gen_train_df(train_events, run_id):
517    """
518    Generates a DataFrame from the train events and saves it to a CSV file.
519    Args:
520        train_events (dict): Dictionary containing train events where keys are event names and values are lists of event data.
521        run_id (str): Unique identifier for the run to save the DataFrame.
522    Returns:
523        pd.DataFrame: DataFrame containing the train events.
524    """
525    train_events_df = pd.DataFrame(train_events).T
526    train_events_df.to_csv(f'.{run_id}/logs/train_logs.csv')
527    return train_events_df

Generates a DataFrame from the train events and saves it to a CSV file.

Arguments:
  • train_events (dict): Dictionary containing train events where keys are event names and values are lists of event data.
  • run_id (str): Unique identifier for the run to save the DataFrame.
Returns:

pd.DataFrame: DataFrame containing the train events.

def get_dists(run_id, plot=True):
534def get_dists(run_id, plot=True):
535    """
536    Analyzes the ship logs to calculate the mean, min, max, and standard deviation of various time distributions for each terminal type.
537    Args:
538        run_id (str): Unique identifier for the run to save the analysis results.
539        plot (bool): Whether to generate and save plots of the time distributions.
540    Returns:
541        tuple: A tuple containing the mean, min, max, and standard deviation DataFrames for each terminal type.
542    """
543    def extract_t(restriction_str):
544        if isinstance(restriction_str, str):
545            parts = restriction_str.split(", ")
546            for part in parts:
547                if part.startswith("T"):
548                    return float(part.split(":")[1])
549        return 0.0
550
551    data = pd.read_excel(f'.{run_id}/logs/ship_logs.xlsx', sheet_name='Sheet1')
552
553    data['Restriction In (T)'] = data['Time for Restriction In'].apply(
554        extract_t)
555    data['Restriction Out (T)'] = data['Time for Restriction Out'].apply(
556        extract_t)
557
558    data['Channel In'] = data['Time to Common Channel In'] + \
559        data['Time to Travel Channel In']
560    data['Channel Out'] = data['Time to Common Channel Out'] + \
561        data['Time to Travel Channel Out']
562    data['Terminal Ops'] = data['Loading Time'] + data['Unloading Time']
563    data['Terminal Other'] = data['Waiting Time'] + data['Departure Time']
564
565    columns = [
566        'Time to get Berth', 'Restriction In (T)', 'Channel In', 'Time to get Pilot In',
567        'Time to get Tugs In', 'Terminal Ops', 'Terminal Other', 'Restriction Out (T)',
568        'Time to get Pilot Out', 'Time to get Tugs Out', 'Channel Out'
569    ]
570
571    mean_values = data.groupby('Terminal Type')[columns].mean()
572    min_values = data.groupby('Terminal Type')[columns].min()
573    max_values = data.groupby('Terminal Type')[columns].max()
574    std_values = data.groupby('Terminal Type')[columns].std()
575
576    error_bars = {'min': mean_values - min_values,
577                  'max': max_values - mean_values}
578
579    with open(f'.{run_id}/logs/final_report.txt', 'a') as file:
580        file.write("Mean Time Distribution:\n")
581        file.write(mean_values.to_string())
582        file.write("\n\nMin Time Distribution:\n")
583        file.write(min_values.to_string())
584        file.write("\n\nMax Time Distribution:\n")
585        file.write(max_values.to_string())
586        file.write("\n\nStandard Deviation:\n")
587        file.write(std_values.to_string())
588
589    if plot:
590        for terminal in mean_values.index:
591            plt.figure(figsize=(10, 6))
592            means = mean_values.loc[terminal]
593            min_err = error_bars['min'].loc[terminal]
594            max_err = error_bars['max'].loc[terminal]
595            stds = std_values.loc[terminal]
596
597            plt.errorbar(columns, means, yerr=[
598                         min_err, max_err], fmt='-o', capsize=5)
599
600            for i, (mean, min_e, max_e, std) in enumerate(zip(means, min_err, max_err, stds)):
601                plt.annotate(
602                    f'Mean: {mean:.1f}\nSD: {std:.1f}',
603                    (i, mean),
604                    textcoords="offset points",
605                    xytext=(0, 10),
606                    ha='center'
607                )
608
609            plt.xticks(rotation=90)
610            plt.title(f'{terminal} Terminal: Mean with Min/Max and SD')
611            plt.xlabel('Processes')
612            plt.ylabel('Time (Units)')
613            plt.tight_layout()
614            # os.makedirs('/plots/TerminalProcessDist/', exist_ok=True)
615            plt.savefig(
616                f'.{run_id}/plots/TerminalProcessDist/{terminal}_terminal_plot.png')
617            plt.close()
618    return mean_values, min_values, max_values, std_values

Analyzes the ship logs to calculate the mean, min, max, and standard deviation of various time distributions for each terminal type.

Arguments:
  • run_id (str): Unique identifier for the run to save the analysis results.
  • plot (bool): Whether to generate and save plots of the time distributions.
Returns:

tuple: A tuple containing the mean, min, max, and standard deviation DataFrames for each terminal type.

def plot_channel(run_id):
626def plot_channel(run_id):
627    """
628    Plots the channel sections and their respective terminals.
629    Args:
630        run_id (str): Unique identifier for the run to save the plot.
631    Returns:
632        None: Saves the plot as a PDF file in the specified directory.
633    """
634
635    # Normalize the draft values for color mapping
636    channel_sections = constants.CHANNEL_SECTION_DIMENSIONS
637    last_section_dict = constants.LAST_SECTION_DICT
638    drafts = [draft for _, _, draft, _ in channel_sections.values()]
639    norm = mcolors.Normalize(vmin=min(drafts), vmax=max(drafts))
640    cmap = plt.cm.viridis
641    fig, ax = plt.subplots(figsize=(20, 8))
642    max_width = max(width for _, width, _, _ in channel_sections.values())
643
644    # Calculate cumulative lengths for x positions (rotated plot)
645    cumulative_length = 0
646    x_positions = {}
647    for section, (length, width, draft, speed) in channel_sections.items():
648        x_positions[section] = cumulative_length + \
649            length / 2  # Middle of the section
650        cumulative_length += length
651
652    for section, (length, width, draft, speed) in channel_sections.items():
653        color = cmap(norm(draft))
654        y_pos = (max_width - width) / 2  # Center-align the rectangle
655        ax.add_patch(plt.Rectangle(
656            (x_positions[section] - length / 2, y_pos), length, width, edgecolor='black', facecolor=color))
657        terminal_num = next((f'{terminal_type} Terminal : #{terminal}' for terminal_type, terminals in last_section_dict.items(
658        ) for terminal, sec in terminals.items() if sec == section), None)
659        annotation = f'Section {section}'
660        if terminal_num:
661            annotation += f'\n{terminal_num}'
662        ax.text(x_positions[section], max_width / 2, annotation,
663                ha='center', va='center', color='white', fontsize=8, rotation=90)
664
665    cbar = plt.colorbar(plt.cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax)
666    cbar.set_label('Depth (Draft)')
667
668    ax.set_xlim(0, cumulative_length)
669    ax.set_ylim(0, max_width + 5)
670    ax.set_xlabel('Length')
671    ax.set_ylabel('Breadth')
672    ax.set_title('Channel Sections and Terminals')
673    plt.savefig(f'.{run_id}/plots/ChannelSections.pdf')
674    plt.close()

Plots the channel sections and their respective terminals.

Arguments:
  • run_id (str): Unique identifier for the run to save the plot.
Returns:

None: Saves the plot as a PDF file in the specified directory.

def ship_data_csv_1(run_id):
677def ship_data_csv_1(run_id):
678    """
679    Parses the ship data from the log file and saves it to a CSV file. 
680    The CSV file contains information about each ship's entry and exit times in the channel sections.
681    Args:
682        run_id (str): Unique identifier for the run to save the CSV file.
683    Returns:
684        pd.DataFrame: DataFrame containing the parsed ship data.
685    """
686    ship_data = []
687    file_path = f'.{run_id}/logs/ships_in_channel.txt'
688
689    with open(file_path, 'r') as file:
690        for line in file:
691            match = re.search(
692                r"Ship (\d+) of type (\w+) of width ([\d.]+) and draft ([\d.]+) spent from ([\d.]+) to ([\d.]+) in section (\d+) going (\w+)", line)
693            if match:
694                ship_data.append({
695                    "ship": int(match.group(1)),
696                    "ship_type": match.group(2),
697                    "width": float(match.group(3)),
698                    "draft": float(match.group(4)),
699                    "section": int(match.group(7)),
700                    "start": float(match.group(5)),
701                    "end": float(match.group(6)),
702                    "direction": match.group(8)
703                })
704            else:
705                print(f"Error parsing line: {line}")
706
707    df = pd.DataFrame(ship_data)
708    df = df.sort_values(by=['ship', 'direction', 'section'])
709
710    df.to_csv(
711        f'.{run_id}/logs/channel_section_entry_exit_times.csv', index=False)
712
713    return df

Parses the ship data from the log file and saves it to a CSV file. The CSV file contains information about each ship's entry and exit times in the channel sections.

Arguments:
  • run_id (str): Unique identifier for the run to save the CSV file.
Returns:

pd.DataFrame: DataFrame containing the parsed ship data.

def ship_data_csv_2(run_id):
716def ship_data_csv_2(run_id):
717    """
718    Parses the ship data from the log file and saves it to a CSV file with time spent in each section.
719    Args:
720        run_id (str): Unique identifier for the run to save the CSV file.
721    Returns:
722        pd.DataFrame: DataFrame containing the parsed ship data with time spent in each section.
723    """
724    ship_data = {}
725    file_path = f'.{run_id}/logs/ships_in_channel.txt'
726    pattern = r"Ship (\d+) of type (\w+) of width ([\d.]+) and draft ([\d.]+) spent from ([\d.]+) to ([\d.]+) in section (\d+) going (\w+)"
727
728    with open(file_path, 'r') as file:
729        for line in file:
730            match = re.search(pattern, line)
731            if match:
732                ship_num = int(match.group(1))
733                ship_dir = match.group(8)
734                ship_type = match.group(2)
735                section = int(match.group(7))
736                time_spent = float(match.group(6)) - float(match.group(5))
737
738                if (ship_num, ship_dir) not in ship_data:
739                    ship_data[(ship_num, ship_dir)] = {}
740
741                ship_data[(ship_num, ship_dir)][section] = time_spent
742            else:
743                print(f"Error parsing line: {line}")
744
745        max_section = max(max(ship_info.keys())
746                          for ship_info in ship_data.values())
747        df = pd.DataFrame(ship_data).T
748        df = df.reindex(columns=range(max_section + 1)).fillna(np.nan)
749        df.columns = [f"Time in Sec {i}" for i in range(max_section + 1)]
750        df['Total Time in Channel'] = df.sum(axis=1)
751        df['Ship'] = [ship[0] for ship in df.index]
752        df['Direction'] = [ship[1] for ship in df.index]
753        section_columns = [f"Time in Sec {i}" for i in range(max_section + 1)]
754        df = df[['Ship', 'Direction'] +
755                ['Total Time in Channel'] + section_columns]
756        df = df.sort_values(by=['Ship', 'Direction'])
757        df.to_csv(f'.{run_id}/logs/ships_time_in_channel.csv', index=False)
758
759        return df

Parses the ship data from the log file and saves it to a CSV file with time spent in each section.

Arguments:
  • run_id (str): Unique identifier for the run to save the CSV file.
Returns:

pd.DataFrame: DataFrame containing the parsed ship data with time spent in each section.

def ships_in_channel_analysis(run_id, plot=True):
762def ships_in_channel_analysis(run_id, plot=True):
763    """
764    Analyzes the ships in the channel by reading the ship data from a text file, calculating the time spent in each section, and generating a time series of ships in the channel.
765    Args:
766        run_id (str): Unique identifier for the run to save the analysis results.
767        plot (bool): Whether to generate and save plots of the channel utilization.
768    Returns:
769        None: Saves the analysis results to CSV files and generates plots if `plot` is True.
770    """
771
772    ship_data_csv_1(run_id)
773    ship_data_csv_2(run_id)
774
775    os.remove(f'.{run_id}/logs/ships_in_channel.txt')
776
777    file_path = f'.{run_id}/logs/channel_section_entry_exit_times.csv'
778    ships_data = pd.read_csv(file_path)
779    ships_data['time_in_section'] = ships_data['end'] - ships_data['start']
780    max_time = ships_data['end'].max()
781
782    section_time_series = pd.DataFrame(0, index=np.arange(
783        max_time)-1, columns=range(ships_data['section'].max() + 1))
784
785    for _, row in ships_data.iterrows():
786        section_time_series.loc[row['start']:row['end'], row['section']] += 1
787
788    # save section time series to csv
789    section_time_series.to_csv(f'.{run_id}/logs/section_time_series.csv')
790    total_ships_in_channel_full = section_time_series.sum(axis=1)
791    mean_ships_per_time_step = section_time_series.loc[constants.WARMUP_ITERS:].mean(
792    )
793    total_ships_in_channel = section_time_series.sum(axis=1)
794    mean_total_ships_in_channel = total_ships_in_channel.mean()
795
796    # save total ships in channel to csv
797    total_ships_in_channel.to_csv(f'.{run_id}/logs/total_ships_in_channel.csv')
798
799    if plot:
800        plt.plot(total_ships_in_channel_full)
801        plt.xticks(fontsize=12)
802        plt.yticks(fontsize=12)
803        plt.xlim(left=0)
804        plt.ylim(bottom=0)
805        plt.title('Ships in channel', fontsize=15)
806        plt.xlabel('Time (hr)', fontsize=15)
807        plt.ylabel('Number of ships', fontsize=15)
808        plt.grid(True)
809        plt.tight_layout()
810        plt.savefig(f'.{run_id}/plots/channel_utilization.pdf')
811        plt.close()
812
813    report_path = f".{run_id}/logs/final_report.txt"
814    with open(report_path, 'a') as f:
815        f.write(
816            f"Mean number of vessels in channel: {mean_total_ships_in_channel}\n\n")
817        f.write("Mean number of vessel in each section:\n")
818        for section, mean_ships in mean_ships_per_time_step.items():
819            f.write(f"Section {section}: {mean_ships}\n")
820        f.write("\n")
821
822    # Calculate entry and exit times for each ship
823    file_path = f'.{run_id}/logs/channel_section_entry_exit_times.csv'
824    ships_data = pd.read_csv(file_path)
825
826    # Identify the first and last sections for each ship
827    first_section = ships_data[ships_data['section'] == 0]
828    last_sections = ships_data.groupby(['ship', 'direction'])[
829        'section'].max().reset_index()
830
831    # Merge last sections with the original dataset to get exit times
832    last_section_exit = ships_data.merge(
833        last_sections,
834        on=['ship', 'section', 'direction'],
835        how='inner'
836    )
837
838    # Get entry and exit times for each ship
839    entry_exit_times = pd.merge(
840        first_section[['ship', 'direction', 'start']].rename(
841            columns={'start': 'entry_time_section_0'}),
842        last_section_exit[['ship', 'direction', 'end']].rename(
843            columns={'end': 'exit_time_last_section'}),
844        on=['ship', 'direction'],
845        how='outer'
846    )
847
848    # Save the entry and exit times to csv
849    entry_exit_times.to_csv(
850        f'.{run_id}/logs/entry_exit_times.csv', index=False)

Analyzes the ships in the channel by reading the ship data from a text file, calculating the time spent in each section, and generating a time series of ships in the channel.

Arguments:
  • run_id (str): Unique identifier for the run to save the analysis results.
  • plot (bool): Whether to generate and save plots of the channel utilization.
Returns:

None: Saves the analysis results to CSV files and generates plots if plot is True.

def ships_in_anchorage_analysis(ship_type, run_id, plot=True):
853def ships_in_anchorage_analysis(ship_type, run_id, plot=True):
854    """
855    Analyzes the ships in anchorage by reading the ship data from a CSV file, calculating the cumulative arrivals and channel entries, and generating a time series of ships waiting in anchorage.
856    Args:
857        ship_type (str): Type of ships to analyze ('all', 'Container', 'Liquid', 'DryBulk').
858        run_id (str): Unique identifier for the run to save the analysis results.
859        plot (bool): Whether to generate and save plots of the anchorage queue.
860    Returns:
861        None: Saves the analysis results to CSV files and generates plots if `plot` is True.
862    """
863
864    # time series of ships that entered anchorage
865    input_ship_data = pd.read_csv(f'.{run_id}/logs/ship_data.csv')
866
867    if ship_type == 'all':
868        input_ship_data = input_ship_data
869    else:
870        input_ship_data = input_ship_data[input_ship_data['ship_type'] == ship_type]
871
872    arrival_times = input_ship_data['arrival'].dropna(
873    ).sort_values().reset_index(drop=True)
874    cumulative_arrivals = pd.DataFrame(
875        {'time_index': range(1, int(arrival_times.max()) + 1)})
876    cumulative_arrivals['cumulative_arrivals'] = cumulative_arrivals['time_index'].apply(
877        lambda x: (arrival_times <= x).sum()
878    )
879
880    # save the cumulative arrivals to csv
881    cumulative_arrivals.to_csv(f'.{run_id}/logs/cumulative_arrivals.csv')
882
883    # time series of ships that entered channel
884    file_path = f'.{run_id}/logs/channel_section_entry_exit_times.csv'
885    ships_data = pd.read_csv(file_path)
886
887    if ship_type != 'all':
888        ships_data = ships_data[ships_data['ship_type'] == ship_type]
889
890    ships_data = ships_data[ships_data['direction'] == 'in']
891    ships_data = ships_data.groupby('ship').first().reset_index()
892    channel_enter_times = ships_data['start'].reset_index(drop=True)
893    cumulative_channel_enter = pd.DataFrame(
894        {'time_index': range(1, int(constants.SIMULATION_TIME) + 1)})
895    cumulative_channel_enter['cumulative_channel_enter'] = cumulative_channel_enter['time_index'].apply(
896        lambda x: (channel_enter_times <= x).sum()
897    )
898
899    # combine the two dfs
900    combined = cumulative_arrivals.merge(
901        cumulative_channel_enter, on='time_index', how='outer')
902    combined['waiting_in_anchorage'] = combined['cumulative_arrivals'] - \
903        combined['cumulative_channel_enter']
904
905    # svae the combined dataframe to csv
906    combined.to_csv(f'.{run_id}/logs/waiting_in_anchorage_{ship_type}.csv')
907
908    # Calculate the total number of rows
909    rows_in_combined = len(combined)
910    mean_waiting_in_anchorage = combined.loc[constants.WARMUP_ITERS:]['waiting_in_anchorage'].mean(
911    )
912
913    # plot time series of waiting in anchorage
914    if plot:
915        plt.plot(combined['time_index'], combined['waiting_in_anchorage'])
916        plt.title(f'Anchorage queue - {ship_type} ships', fontsize=15)
917        plt.xticks(fontsize=12)
918        plt.yticks(fontsize=12)
919        plt.xlim(left=0)
920        plt.ylim(bottom=0)
921        plt.xlabel('Time (hr)', fontsize=15)
922        plt.ylabel('Number of ships', fontsize=15)
923        plt.grid(True)
924        plt.tight_layout()
925    plt.savefig(f'.{run_id}/plots/anchorage_queue_{ship_type}.pdf')
926
927    with open(f'.{run_id}/logs/final_report.txt', 'a') as f:
928        f.write(
929            f"Mean number of {ship_type} ships waiting in anchorage: {mean_waiting_in_anchorage}\n\n")
930
931    plt.close()
932
933    return

Analyzes the ships in anchorage by reading the ship data from a CSV file, calculating the cumulative arrivals and channel entries, and generating a time series of ships waiting in anchorage.

Arguments:
  • ship_type (str): Type of ships to analyze ('all', 'Container', 'Liquid', 'DryBulk').
  • run_id (str): Unique identifier for the run to save the analysis results.
  • plot (bool): Whether to generate and save plots of the anchorage queue.
Returns:

None: Saves the analysis results to CSV files and generates plots if plot is True.

def create_truck_csv(run_id):
940def create_truck_csv(run_id):
941    """
942    Read truck_data.txt, parse into a DataFrame, save as CSV under ./<run_id>/truck_data.csv,
943    and return the DataFrame.
944    Args:
945        run_id (str): Unique identifier for the run to save the truck data.
946    Returns:
947        pd.DataFrame: DataFrame containing the truck data with columns for truck ID, start time, dwell time, terminal ID, terminal type, and arrival time.
948    """
949    txt_path = f'.{run_id}/logs/truck_data.txt'
950    data = []
951
952    # only take first 100000 units of data
953    count = 0
954    with open(txt_path, 'r') as f:
955        for line in f:
956            count += 1
957            if count >= 100000:
958                break
959            line = line.strip()
960            if not line:
961                continue
962            parts = [p.strip() for p in line.split(',')]
963            row = {}
964            for part in parts:
965                key, val = part.split(':', 1)
966                row[key.strip()] = val.strip()
967            data.append(row)
968
969    df = pd.DataFrame(data)
970    # convert types
971    df = df.astype({
972        'truck_id'     : int,
973        'start_time'   : float,
974        'dwell_time'   : float,
975        'terminal_id'  : int,
976        'terminal_type': str
977    })
978    # compute arrival-of-day
979    df['arrival_time'] = df['start_time'] % 24
980
981    # make run directory & save CSV
982    out_dir = f'.{run_id}/logs/'
983    os.makedirs(out_dir, exist_ok=True)
984    csv_path = os.path.join(out_dir, 'truck_data.csv')
985    df.to_csv(csv_path, index=False)
986
987    return df

Read truck_data.txt, parse into a DataFrame, save as CSV under .//truck_data.csv, and return the DataFrame.

Arguments:
  • run_id (str): Unique identifier for the run to save the truck data.
Returns:

pd.DataFrame: DataFrame containing the truck data with columns for truck ID, start time, dwell time, terminal ID, terminal type, and arrival time.

def plot_dwell_by_type(run_id):
 990def plot_dwell_by_type(run_id):
 991    """
 992    Plots the dwell time distribution of trucks by terminal type.   
 993    Args:
 994        run_id (str): Unique identifier for the run to save the plots.
 995    Returns:
 996        None: Saves the plots as PDF files in the specified directories.
 997    """
 998    out_dir = f'.{run_id}/plots/TruckDwellByCargo'
 999    df = pd.read_csv(f'.{run_id}/logs/truck_data.csv')
1000    os.makedirs(out_dir, exist_ok=True)
1001
1002    for ttype, grp in df.groupby('terminal_type'):
1003        plt.figure()
1004        grp['dwell_time'].hist(bins=30)
1005        plt.title(f'Dwell Time Distribution — {ttype}')
1006        plt.xlabel('Dwell Time')
1007        plt.ylabel('Frequency')
1008        # make the y ticks as percentage
1009        plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x/len(grp)*100:.1f}%'))
1010        plt.xticks(rotation=45)
1011        plt.grid(axis='y', linestyle='--', alpha=0.7)
1012        # save the plot
1013        plt.tight_layout()
1014        fname = f'dwell_distribution_{ttype}.pdf'
1015        plt.savefig(os.path.join(out_dir, fname))
1016        # plt.show()
1017        plt.close()

Plots the dwell time distribution of trucks by terminal type.

Arguments:
  • run_id (str): Unique identifier for the run to save the plots.
Returns:

None: Saves the plots as PDF files in the specified directories.

def plot_dwell_by_terminal(run_id):
1020def plot_dwell_by_terminal(run_id):
1021    """
1022    Plots the dwell time distribution of trucks by terminal type and terminal ID.
1023    Args:
1024        run_id (str): Unique identifier for the run to save the plots.          
1025    Returns:
1026        None: Saves the plots as PDF files in the specified directories.
1027    """
1028    df = pd.read_csv(f'.{run_id}/logs/truck_data.csv')
1029    dir_term  = f'.{run_id}/plots/TruckDwellByTerminal'
1030    os.makedirs(dir_term,  exist_ok=True)
1031
1032    for (ttype, tid), grp in df.groupby(['terminal_type','terminal_id']):
1033        plt.figure()
1034        grp['dwell_time'].hist(bins=30)
1035        plt.title(f'Dwell Time — {ttype} (Terminal {tid})')
1036        plt.xlabel('Dwell Time')
1037        plt.ylabel('Frequency')
1038        # make the y ticks as percentage
1039        plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x/len(grp)*100:.1f}%'))
1040        plt.xticks(rotation=45)
1041        plt.grid(axis='y', linestyle='--', alpha=0.7)
1042        plt.tight_layout()
1043        fname = f'dwell_distribution_{ttype}_{tid}.pdf'
1044        plt.savefig(os.path.join(dir_term,  fname))
1045        plt.close()

Plots the dwell time distribution of trucks by terminal type and terminal ID.

Arguments:
  • run_id (str): Unique identifier for the run to save the plots.
Returns:

None: Saves the plots as PDF files in the specified directories.

def plot_arrival_distributions(run_id):
1048def plot_arrival_distributions(run_id):
1049    """
1050    Plots the arrival time distribution of trucks by terminal type and terminal ID.
1051    Args:
1052        run_id (str): Unique identifier for the run to save the plots.
1053    Returns:
1054        None: Saves the plots as JPEG files in the specified directories.   
1055    """
1056    df = pd.read_csv(f'.{run_id}/logs/truck_data.csv')
1057    dir_type  = f'.{run_id}/plots/TruckArrivalByCargo'
1058    dir_cargo = f'.{run_id}/plots/TruckArrivalByTerminal'
1059    os.makedirs(dir_type,  exist_ok=True)
1060    os.makedirs(dir_cargo, exist_ok=True)
1061
1062    # by type
1063    for ttype, grp in df.groupby('terminal_type'):
1064        plt.figure()
1065        grp['arrival_time'].hist(bins=24, range=(0,24))
1066        plt.title(f'Arrival Time Distribution — {ttype}')
1067        plt.xlabel('Hour of Day')
1068        plt.ylabel('Frequency')
1069        # make the y ticks as percentage
1070        plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x/len(grp)*100:.1f}%'))
1071        plt.xticks(rotation=45)
1072        plt.grid(axis='y', linestyle='--', alpha=0.7)
1073        plt.tight_layout()
1074        fname = f'arrival_distribution_{ttype}.jpg'
1075        plt.savefig(os.path.join(dir_type, fname))
1076        plt.close()
1077
1078    # by terminal
1079    for (ttype, tid), grp in df.groupby(['terminal_type','terminal_id']):
1080        plt.figure()
1081        grp['arrival_time'].hist(bins=24, range=(0,24))
1082        plt.title(f'Arrival Time — {ttype} (Terminal {tid})')
1083        plt.xlabel('Hour of Day')
1084        plt.ylabel('Frequency')
1085        # make the y ticks as percentage
1086        plt.gca().yaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x/len(grp)*100:.1f}%'))
1087        plt.xticks(rotation=45)
1088        plt.grid(axis='y', linestyle='--', alpha=0.7)
1089        plt.tight_layout()
1090        fname = f'arrival_distribution_{ttype}_{tid}.jpg'
1091        plt.savefig(os.path.join(dir_cargo, fname))
1092        plt.close()

Plots the arrival time distribution of trucks by terminal type and terminal ID.

Arguments:
  • run_id (str): Unique identifier for the run to save the plots.
Returns:

None: Saves the plots as JPEG files in the specified directories.

def delete_truck_data_csv(run_id):
1095def delete_truck_data_csv(run_id):
1096    """
1097    Delete the truck_data.csv file if it exists and save the truck data as a pickle file.
1098    Args:
1099        run_id (str): Unique identifier for the run to save the truck data as a pickle file.
1100    Returns:
1101        None: Deletes the truck_data.csv file and saves the truck data as a pickle file.
1102    """
1103    csv_path = f'.{run_id}/logs/truck_data.csv'
1104    pkl_path = f'.{run_id}/logs/truck_data.pkl'
1105    df = pd.read_csv(csv_path)
1106    df.to_pickle(pkl_path)

Delete the truck_data.csv file if it exists and save the truck data as a pickle file.

Arguments:
  • run_id (str): Unique identifier for the run to save the truck data as a pickle file.
Returns:

None: Deletes the truck_data.csv file and saves the truck data as a pickle file.

def update_ship_logs( ship_logs, ship_type, ship_id, selected_terminal, ship_start_time='nan', time_to_get_berth='nan', time_for_restriction_in='nan', time_to_get_pilot_in='nan', time_to_get_tugs_in='nan', time_to_common_channel_in='nan', time_to_travel_channel_in='nan', time_to_tug_steer_in='nan', unloading_time='nan', loading_time='nan', waiting_time='nan', departure_time='nan', time_to_get_pilot_out='nan', time_to_get_tugs_out='nan', time_to_tug_steer_out='nan', time_for_restriction_out='nan', time_for_uturn='nan', time_to_travel_channel_out='nan', time_to_common_channel_out='nan', ship_end_time='nan'):
1115def update_ship_logs(ship_logs, ship_type, ship_id, selected_terminal, ship_start_time="nan", time_to_get_berth="nan", time_for_restriction_in="nan", time_to_get_pilot_in="nan",
1116                     time_to_get_tugs_in="nan", time_to_common_channel_in="nan", time_to_travel_channel_in="nan", time_to_tug_steer_in="nan", unloading_time="nan", loading_time="nan", waiting_time="nan",
1117                     departure_time="nan", time_to_get_pilot_out="nan", time_to_get_tugs_out="nan", time_to_tug_steer_out="nan", time_for_restriction_out='nan', time_for_uturn="nan",
1118                     time_to_travel_channel_out="nan", time_to_common_channel_out="nan", ship_end_time="nan"):
1119    """
1120    Updates the ship logs with the provided information. If a log with the same Ship ID exists, it updates the existing log; otherwise, it appends a new log.
1121    Note 1: The ship type is represented by a single character (e.g., "C" for Container, "L" for Liquid, "D" for Dry Bulk).
1122    Note 2: If the log already exists, it updates the existing log with the new values, ignoring 'nan' values.
1123    Args:
1124        ship_logs (list): List of existing ship logs.
1125        ship_type (str): Type of the ship (e.g., "C" for Container, "L" for Liquid, "D" for Dry Bulk).
1126        ship_id (int): Unique identifier for the ship.
1127        selected_terminal (str): The terminal where the ship is directed.
1128        ship_start_time (str): Start time of the ship's operation.
1129        time_to_get_berth (str): Time taken to get to the berth.
1130        time_for_restriction_in (str): Time for restriction in.
1131        time_to_get_pilot_in (str): Time taken to get the pilot in.
1132        time_to_get_tugs_in (str): Time taken to get tugs in.
1133        time_to_common_channel_in (str): Time taken to get to the common channel in.
1134        time_to_travel_channel_in (str): Time taken to travel the channel in.
1135        time_to_tug_steer_in (str): Time taken for tug steering in.
1136        unloading_time (str): Time taken for unloading.
1137        loading_time (str): Time taken for loading.
1138        waiting_time (str): Time spent waiting.
1139        departure_time (str): Departure time of the ship.
1140        time_to_get_pilot_out (str): Time taken to get the pilot out.
1141        time_to_get_tugs_out (str): Time taken to get tugs out.
1142        time_to_tug_steer_out (str): Time taken for tug steering out.
1143        time_for_restriction_out (str): Time for restriction out.
1144        time_for_uturn (str): Time taken for U-turn.
1145        time_to_travel_channel_out (str): Time taken to travel the channel out.
1146        time_to_common_channel_out (str): Time taken to get to the common channel out.
1147        ship_end_time (str): End time of the ship's operation.
1148    Returns:
1149        list: Updated ship logs with the new or modified log entry.
1150    """
1151
1152    if ship_type == "C":
1153        ship_type_full = "Container"
1154    elif ship_type == "L":
1155        ship_type_full = "Liquid"
1156    elif ship_type == "D":
1157        ship_type_full = "Dry Bulk"
1158
1159    # Create the log entry
1160    new_log = [
1161        f"Ship_{ship_id}",
1162        ship_type_full,
1163        f"{ship_type}.{selected_terminal}",
1164        ship_start_time,
1165        time_to_get_berth,
1166        time_for_restriction_in,
1167        time_to_get_pilot_in,
1168        time_to_get_tugs_in,
1169        time_to_common_channel_in,
1170        time_to_travel_channel_in,
1171        time_to_tug_steer_in,
1172        unloading_time,
1173        loading_time,
1174        waiting_time,
1175        departure_time,
1176        time_to_get_pilot_out,
1177        time_to_get_tugs_out,
1178        time_for_uturn,
1179        time_to_tug_steer_out,
1180        time_for_restriction_out,
1181        time_to_travel_channel_out,
1182        time_to_common_channel_out,
1183        None,
1184        None,
1185        ship_end_time
1186    ]
1187
1188    # Check if the log with the same Ship ID exists
1189    for i, log in enumerate(ship_logs):
1190        if log[0] == f"Ship_{ship_id}":
1191            for j, vals in enumerate(new_log):
1192                if new_log[j] != 'nan':
1193                    ship_logs[i][j] = vals
1194
1195            return ship_logs
1196
1197    # If no existing log is found, append the new log
1198    ship_logs.append(new_log)
1199
1200    return ship_logs

Updates the ship logs with the provided information. If a log with the same Ship ID exists, it updates the existing log; otherwise, it appends a new log. Note 1: The ship type is represented by a single character (e.g., "C" for Container, "L" for Liquid, "D" for Dry Bulk). Note 2: If the log already exists, it updates the existing log with the new values, ignoring 'nan' values.

Arguments:
  • ship_logs (list): List of existing ship logs.
  • ship_type (str): Type of the ship (e.g., "C" for Container, "L" for Liquid, "D" for Dry Bulk).
  • ship_id (int): Unique identifier for the ship.
  • selected_terminal (str): The terminal where the ship is directed.
  • ship_start_time (str): Start time of the ship's operation.
  • time_to_get_berth (str): Time taken to get to the berth.
  • time_for_restriction_in (str): Time for restriction in.
  • time_to_get_pilot_in (str): Time taken to get the pilot in.
  • time_to_get_tugs_in (str): Time taken to get tugs in.
  • time_to_common_channel_in (str): Time taken to get to the common channel in.
  • time_to_travel_channel_in (str): Time taken to travel the channel in.
  • time_to_tug_steer_in (str): Time taken for tug steering in.
  • unloading_time (str): Time taken for unloading.
  • loading_time (str): Time taken for loading.
  • waiting_time (str): Time spent waiting.
  • departure_time (str): Departure time of the ship.
  • time_to_get_pilot_out (str): Time taken to get the pilot out.
  • time_to_get_tugs_out (str): Time taken to get tugs out.
  • time_to_tug_steer_out (str): Time taken for tug steering out.
  • time_for_restriction_out (str): Time for restriction out.
  • time_for_uturn (str): Time taken for U-turn.
  • time_to_travel_channel_out (str): Time taken to travel the channel out.
  • time_to_common_channel_out (str): Time taken to get to the common channel out.
  • ship_end_time (str): End time of the ship's operation.
Returns:

list: Updated ship logs with the new or modified log entry.

def analyse_bays_chassis_utilization(chassis_bays_utilization, num_terminals_list, run_id):
1203def analyse_bays_chassis_utilization(chassis_bays_utilization, num_terminals_list, run_id):
1204    """
1205    Generates plots for chassis and bay utilization at each terminal type.
1206    Args:
1207        chassis_bays_utilization (dict): Dictionary containing chassis and bay utilization data for each terminal type.
1208        num_terminals_list (list): List containing the number of terminals for each terminal type.
1209        run_id (str): Unique identifier for the run to save the plots.
1210    Returns:
1211        None: Saves the plots as PDF files in the specified directory.
1212    """
1213    for terminal_type in ["Container", "Liquid", "DryBulk"]:
1214        for terminal_id in range(1, num_terminals_list[["Container", "Liquid", "DryBulk"].index(terminal_type)] + 1):
1215            to_plot = chassis_bays_utilization[terminal_type][terminal_id]
1216
1217            x_vals = []
1218            y_vals = []
1219
1220            for vals in to_plot:
1221                x_vals.append(vals[0])
1222                y_vals.append(vals[1])
1223
1224            plt.figure(figsize=(12, 8))
1225            plt.plot(x_vals, y_vals)
1226            plt.title(
1227                f'Chassis / Bay Utilization at {terminal_type} Terminal {terminal_id}')
1228            plt.xlabel('Time')
1229            plt.ylabel('Utilization')
1230            plt.savefig(
1231                f'.{run_id}/bottlenecks/chassisBays/{terminal_type}_Terminal_{terminal_id}.pdf')
1232            plt.close()

Generates plots for chassis and bay utilization at each terminal type.

Arguments:
  • chassis_bays_utilization (dict): Dictionary containing chassis and bay utilization data for each terminal type.
  • num_terminals_list (list): List containing the number of terminals for each terminal type.
  • run_id (str): Unique identifier for the run to save the plots.
Returns:

None: Saves the plots as PDF files in the specified directory.

def gen_logs_and_plots( run_id, ship_logs, events, chassis_bays_utilization, num_terminals_list, train_events, channel_logs, channel_events, channel, animate=True):
1235def gen_logs_and_plots(run_id, ship_logs, events, chassis_bays_utilization, num_terminals_list, train_events, channel_logs, channel_events, channel, animate=True):
1236    """
1237    Generates logs and plots from the provided ship logs, events, chassis and bay utilization data, train events, and channel logs.
1238    Saves the logs to an Excel file and generates various plots including process charts, dwell times, turn times, truck dwell times, and channel utilization.
1239    Also generates utilization reports for resources such as berths and yards.
1240    The following plots are generated:
1241        - Process chart for ship operations
1242        - Dwell time chart for ships
1243        - Turn time analysis for ships
1244        - Truck dwell times and arrival times at terminals
1245        - Channel utilization over time
1246        - Chassis and bay utilization at each terminal type
1247    The results are saved in the specified run directory.
1248    Args:
1249        run_id (str): Unique identifier for the run to save the logs and plots.
1250        ship_logs (list): List of ship logs containing information about each ship's operations.
1251        events (list): List of events where each event is a tuple containing:
1252                          (event_type, time, name, terminal_id, terminal_type)
1253        chassis_bays_utilization (dict): Dictionary containing chassis and bay utilization data for each terminal type.
1254        num_terminals_list (list): List containing the number of terminals for each terminal type.
1255        train_events (dict): Dictionary containing train events where keys are event names and values are lists of event data.
1256        channel_logs (list): List of channel logs containing information about the channel's operations.
1257        channel_events (list): List of channel events where each event is a tuple containing:
1258                          (event_type, time, name, section_id, section_type)
1259        channel (object): Channel object containing sections and their respective containers.
1260    Returns:
1261        None: Saves the logs and plots in the specified directory.
1262    """
1263    def extract_t(restriction_str):
1264        if isinstance(restriction_str, str):
1265            parts = restriction_str.split(", ")
1266            for part in parts:
1267                if part.startswith("T"):
1268                    return float(part.split(":")[1])
1269        return 0.0
1270
1271    # Ship logs
1272    columns = ["Ship_Id", "Terminal Type", "Terminal Directed", "Start Time", "Time to get Berth", "Time for Restriction In", "Time to get Pilot In", "Time to get Tugs In", "Time to Common Channel In", "Time to Travel Channel In", "Time to Tug Steer In", "Unloading Time",
1273               "Loading Time", "Waiting Time", "Departure Time", "Time to get Pilot Out", "Time to get Tugs Out", "Time for U-Turn", "Time to Tug Steer Out", "Time for Restriction Out", "Time to Travel Channel Out", "Time to Common Channel Out", "Turn Time", "Dwell Time", "End Time"]
1274    df = pd.DataFrame(ship_logs, columns=columns)
1275
1276    # all columns in float format of 2 decimal places
1277    for col in df.columns:
1278        if col not in ["Ship_Id", "Terminal Type", "Terminal Directed", "Time for Restriction In", "Time for Restriction Out"]:
1279            df[col] = df[col].astype(float).round(2)
1280
1281    df['Ship_Id'] = df['Ship_Id'].str.extract(r'(\d+)').astype(int)
1282    df = df.sort_values(by='Ship_Id')
1283    df['Ship_Id'] = "Ship_" + df['Ship_Id'].astype(str)
1284    df['Turn Time'] = df['End Time'] - df['Start Time']
1285    df['Dwell Time'] = df['Unloading Time'] + \
1286        df['Loading Time'] + df['Waiting Time']
1287
1288    df['Restriction In (T)'] = df['Time for Restriction In'].apply(extract_t)
1289    df['Anchorage Time'] = df['Time to get Berth'] + \
1290        df['Restriction In (T)'] + df['Time to get Pilot In'] + \
1291        df['Time to get Tugs In']
1292    # if ship type is container add anchorage waiting_container as a column
1293    for index, row in df.iterrows():
1294        if row['Terminal Type'] == 'Container':
1295            df.at[index, 'Anchorage Time'] += constants.ANCHORAGE_WAITING_CONTAINER
1296        elif row['Terminal Type'] == 'Liquid':
1297            df.at[index, 'Anchorage Time'] += constants.ANCHORAGE_WAITING_LIQUID
1298        elif row['Terminal Type'] == 'Dry Bulk':
1299            df.at[index, 'Anchorage Time'] += constants.ANCHORAGE_WAITING_DRYBULK
1300
1301    df.to_excel(f'.{run_id}/logs/ship_logs.xlsx', index=False)
1302
1303    warmup_period = constants.WARMUP_ITERS / len(df)
1304    mean_anchorage_time = df['Anchorage Time'].sort_values(
1305    ).iloc[constants.WARMUP_ITERS:].mean()
1306    mean_anchorage_time_ctr = df[df['Terminal Type'] == 'Container']['Anchorage Time'].sort_values(
1307    ).iloc[int(warmup_period * len(df[df['Terminal Type'] == 'Container'])):].mean()
1308    mean_anchorage_time_liq = df[df['Terminal Type'] == 'Liquid']['Anchorage Time'].sort_values(
1309    ).iloc[int(warmup_period * len(df[df['Terminal Type'] == 'Liquid'])):].mean()
1310    mean_anchorage_time_db = df[df['Terminal Type'] == 'Dry Bulk']['Anchorage Time'].sort_values(
1311    ).iloc[int(warmup_period * len(df[df['Terminal Type'] == 'Dry Bulk'])):].mean()
1312
1313    report_path = f".{run_id}/logs/final_report.txt"
1314    with open(report_path, 'a') as f:
1315        f.write(f"Mean Anchorage Time: {mean_anchorage_time}\n")
1316        f.write(f"Mean Anchorage Time Container: {mean_anchorage_time_ctr}\n")
1317        f.write(f"Mean Anchorage Time Liquid: {mean_anchorage_time_liq}\n")
1318        f.write(f"Mean Anchorage Time Dry Bulk: {mean_anchorage_time_db}\n\n")
1319
1320    # Generate the process chart
1321    plot_process_chart(events, run_id=run_id)
1322
1323    # Generate the dwell time chart
1324    plot_dwell_times(events, run_id)
1325    analyze_turn_time_ships(run_id)
1326    get_dists(run_id)
1327
1328    # Plot queueus in channel
1329    ships_in_channel_analysis(run_id)
1330    ships_in_anchorage_analysis(ship_type='all', run_id=run_id)
1331    ships_in_anchorage_analysis(ship_type='Container', run_id=run_id)
1332    ships_in_anchorage_analysis(ship_type='DryBulk', run_id=run_id)
1333    ships_in_anchorage_analysis(ship_type='Liquid', run_id=run_id)
1334
1335    # Trucks
1336    create_truck_csv(run_id)
1337    plot_dwell_by_type(run_id)
1338    plot_dwell_by_terminal(run_id)
1339    plot_arrival_distributions(run_id)
1340    delete_truck_data_csv(run_id)
1341
1342    # Bays & Chassis Utilization
1343    analyse_bays_chassis_utilization(
1344        chassis_bays_utilization, num_terminals_list, run_id)
1345
1346    # Train
1347    gen_train_df(train_events, run_id)
1348
1349    # tugs and pilots
1350    tug_pilot_df = pd.read_csv(f'.{run_id}/logs/pilots_tugs_data.csv')
1351
1352    # plot day anf night pilots
1353    plt.figure(figsize=(12, 8))
1354    plt.plot(tug_pilot_df['Time'],
1355             tug_pilot_df['Day Pilots'], label='Day Pilots')
1356    plt.title('Number of day pilots available')
1357    plt.xlabel('Time')
1358    plt.ylabel('Number of pilots')
1359    plt.ylim(0, constants.NUM_PILOTS_DAY[1])
1360    plt.savefig(f'.{run_id}/plots/DayPilots.pdf')
1361    plt.close()
1362
1363    plt.figure(figsize=(12, 8))
1364    plt.plot(tug_pilot_df['Time'],
1365             tug_pilot_df['Night Pilots'], label='Night Pilots')
1366    plt.title('Number of night pilots available')
1367    plt.xlabel('Time')
1368    plt.ylabel('Number of pilots')
1369    plt.ylim(0, constants.NUM_PILOTS_NIGHT[1])
1370    plt.savefig(f'.{run_id}/plots/NightPilots.pdf')
1371    plt.close()
1372
1373    # plot tugboats
1374    plt.figure(figsize=(12, 8))
1375    plt.plot(tug_pilot_df['Time'], tug_pilot_df['Tugboats'], label='Tugboats')
1376    plt.title('Tugboats')
1377    plt.xlabel('Time')
1378    plt.ylabel('Number of tugboats available')
1379    plt.ylim(0, constants.NUM_TUGBOATS[1])
1380    plt.legend()
1381    plt.savefig(f'.{run_id}/plots/Tugboats.pdf')
1382    plt.close()
1383
1384    # Animate the channel usage
1385    if animate:
1386        animate_channel_usage(run_id, channel)
1387
1388    # print("Done!")

Generates logs and plots from the provided ship logs, events, chassis and bay utilization data, train events, and channel logs. Saves the logs to an Excel file and generates various plots including process charts, dwell times, turn times, truck dwell times, and channel utilization. Also generates utilization reports for resources such as berths and yards.

The following plots are generated:
  • Process chart for ship operations
  • Dwell time chart for ships
  • Turn time analysis for ships
  • Truck dwell times and arrival times at terminals
  • Channel utilization over time
  • Chassis and bay utilization at each terminal type

The results are saved in the specified run directory.

Arguments:
  • run_id (str): Unique identifier for the run to save the logs and plots.
  • ship_logs (list): List of ship logs containing information about each ship's operations.
  • events (list): List of events where each event is a tuple containing: (event_type, time, name, terminal_id, terminal_type)
  • chassis_bays_utilization (dict): Dictionary containing chassis and bay utilization data for each terminal type.
  • num_terminals_list (list): List containing the number of terminals for each terminal type.
  • train_events (dict): Dictionary containing train events where keys are event names and values are lists of event data.
  • channel_logs (list): List of channel logs containing information about the channel's operations.
  • channel_events (list): List of channel events where each event is a tuple containing: (event_type, time, name, section_id, section_type)
  • channel (object): Channel object containing sections and their respective containers.
Returns:

None: Saves the logs and plots in the specified directory.

def animate_channel_usage(run_id, channel):
1395def animate_channel_usage(run_id, channel):
1396    """
1397    DEPRICIATED
1398    Animates the channel usage over time, showing width and draft utilization.
1399    Args:
1400        run_id (str): Unique identifier for the run to save the animation.
1401        channel (object): Channel object containing sections and their respective containers.
1402    Returns:
1403        None: Saves the animation as a GIF file in the specified directory.
1404    """
1405    fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))
1406
1407    channel_sections = constants.CHANNEL_SECTION_DIMENSIONS
1408
1409    x_max = len(channel.sections)
1410    y_max = 100
1411
1412    def init():
1413
1414        for i in range(1, len(channel.sections)+1):
1415            ax1.bar(i,  100 * channel.sections[i-1].width_containers[0].level /
1416                    channel.sections[i-1].width_containers[0].capacity, color='b')
1417            ax2.bar(i, 100 * channel.sections[i-1].draft_containers[0].level /
1418                    channel.sections[i-1].draft_containers[0].capacity, color='r')
1419        ax1.set_xlim(0, x_max)
1420        ax1.set_ylim(0, y_max)
1421        ax2.set_xlim(0, x_max)
1422        ax2.set_ylim(0, y_max)
1423        ax1.set_title('Width utilization')
1424        ax2.set_title('Draft utilization')
1425        return ax1.patches + ax2.patches
1426
1427    progress_bar = tqdm(total=int(len(channel.sections[0].width_containers)/2))
1428
1429    def animate(t):
1430
1431        ax1.clear()
1432        ax2.clear()
1433
1434        # print(f'Animating {t}')
1435        progress_bar.update(1)
1436        for i in range(1, len(channel.sections) + 1):
1437            ax1.bar(i,  100 * channel.sections[i-1].width_containers[t].level /
1438                    channel.sections[i-1].width_containers[t].capacity,  color='b')
1439            ax2.bar(i, 100 * channel.sections[i-1].draft_containers[t].level /
1440                    channel.sections[i-1].draft_containers[t].capacity, color='r')
1441            # print(t, i, channel.sections[i-1].width_containers[0].level, channel.sections[i-1].width_containers[0].capacity)
1442
1443        ax1.set_xlim(0, x_max)
1444        ax1.set_ylim(0, y_max)
1445        ax2.set_xlim(0, x_max)
1446        ax2.set_ylim(0, y_max)
1447        fig.suptitle(f'Time Step: {t}', fontsize=16, fontweight='bold')
1448        return ax1.patches + ax2.patches
1449
1450    ani = animation.FuncAnimation(fig, animate, init_func=init, frames=range(
1451        int(len(channel.sections[0].width_containers)/2)), interval=1, blit=False)
1452
1453    # Save the animation as gif
1454    ani.save(f'.{run_id}/animations/channel_avilability.gif',
1455             writer='pillow', fps=1000)
1456    plt.close()

DEPRICIATED Animates the channel usage over time, showing width and draft utilization.

Arguments:
  • run_id (str): Unique identifier for the run to save the animation.
  • channel (object): Channel object containing sections and their respective containers.
Returns:

None: Saves the animation as a GIF file in the specified directory.