Consider the following python code, which attempts to create a neural network LSTM, where timeseries regression is performed. the goal is, given 6 motions of the vessel (heave, heaveVelocity, etc.) as input, predict the associated timesries of cable_tension. It is important to note that the dta structure of the files to train the algorithm is the following: the data from each variable is found in a .txt file where each row is a timestep and each column (delimited by commas) is a different simulation. Therefore, the data was obtained from "N" simulations (N columns). Like this its easy to make the correspondance between values of different variables. However, if all the simaltion data is appended for training the neural network, it will learn from unrealistic discontinuities which correspond to a jump between simulations. In addition, when testing the trained algorithm, these discontinuities will also be present in the test set and may hinder the actual prediction performance of the algorithm. Please make sure that this problem is preoperly addressed and change the code as deemed necessary. The code is: "import pandas as pd
import numpy as np
import pickle
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.layers import LSTM, Dense, Dropout
from tensorflow.keras.callbacks import EarlyStopping, Callback
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
import matplotlib.pyplot as plt
import time

# ===========================
# 1. Data Loading and Preprocessing
# ===========================

# Set simulation data loading option (only "option1" is available now)
simulation_data = "option1"

# Initialize lists to hold features and targets
X_all = []
y_all = []

if simulation_data == "option1":
    # Define file paths for each variable
    file_paths = {
        'heave': 'heave_data.txt',
        'heaveVelocity': 'heaveVelocity_data.txt',
        'surge': 'surge_data.txt',
        'surgeVelocity': 'surgeVelocity_data.txt',
        'sway': 'sway_data.txt',
        'swayVelocity': 'swayVelocity_data.txt',
        'cable_tension': 'compression_max_data.txt'
    }

    # Load each variable's data from the respective text files
    data_dict = {}
    for variable, file_path in file_paths.items():
        data = pd.read_csv(file_path, header=None)
        data_dict[variable] = data  # Each column represents a separate simulation

    num_simulations = data_dict['heave'].shape[1]  # Number of simulations

    # Extract features and targets for each simulation
    for i in range(num_simulations):
        # Extract features for the i-th simulation
        heave = data_dict['heave'].iloc[:, i].values
        heaveVelocity = data_dict['heaveVelocity'].iloc[:, i].values
        surge = data_dict['surge'].iloc[:, i].values
        surgeVelocity = data_dict['surgeVelocity'].iloc[:, i].values
        sway = data_dict['sway'].iloc[:, i].values
        swayVelocity = data_dict['swayVelocity'].iloc[:, i].values

        # Stack features horizontally (shape: [time_steps, 6])
        features = np.column_stack([
            heave,
            heaveVelocity,
            surge,
            surgeVelocity,
            sway,
            swayVelocity
        ])

        # Extract target for the i-th simulation
        target = data_dict['cable_tension'].iloc[:, i].values  # Shape: [time_steps]

        X_all.append(features)
        y_all.append(target)

    # Convert lists to NumPy arrays
    X_all = np.array(X_all)  # Shape: [num_simulations, time_steps, 6]
    y_all = np.array(y_all)  # Shape: [num_simulations, time_steps]

    # ===========================
    # 2. Train-Test Split
    # ===========================

    # Split the data into training and testing sets
    X_train, X_test, y_train, y_test = train_test_split(
        X_all, y_all, test_size=0.2, random_state=42
    )

    # ===========================
    # 3. Data Scaling
    # ===========================

    # Initialize scalers
    scaler_features = MinMaxScaler()
    scaler_target = MinMaxScaler()

    # Flatten the training data to fit the scalers
    X_train_flat = X_train.reshape(-1, X_train.shape[2])  # Shape: [num_sim * time_steps, 6]
    y_train_flat = y_train.flatten().reshape(-1, 1)       # Shape: [num_sim * time_steps, 1]

    # Fit scalers on training data
    scaler_features.fit(X_train_flat)
    scaler_target.fit(y_train_flat)

    # Transform training data
    X_train_scaled = scaler_features.transform(X_train_flat).reshape(X_train.shape)
    y_train_scaled = scaler_target.transform(y_train_flat).reshape(y_train.shape)

    # Transform testing data
    X_test_flat = X_test.reshape(-1, X_test.shape[2])
    y_test_flat = y_test.flatten().reshape(-1, 1)

    X_test_scaled = scaler_features.transform(X_test_flat).reshape(X_test.shape)
    y_test_scaled = scaler_target.transform(y_test_flat).reshape(y_test.shape)

    # Expand y dimensions for LSTM (required shape: [samples, time_steps, 1])
    y_train_scaled = y_train_scaled[:, :, np.newaxis]
    y_test_scaled = y_test_scaled[:, :, np.newaxis]

# ===========================
# 4. Custom Callback for Timing
# ===========================

class TimeHistory(Callback):
    def on_train_begin(self, logs=None):
        self.times = []

    def on_epoch_begin(self, epoch, logs=None):
        self.epoch_time_start = time.time()

    def on_epoch_end(self, epoch, logs=None):
        epoch_time = time.time() - self.epoch_time_start
        self.times.append(epoch_time)
        print(f"Epoch {epoch + 1} time: {epoch_time:.2f} seconds")

time_callback = TimeHistory()

# ===========================
# 5. Building the LSTM Model
# ===========================

model = Sequential()
model.add(LSTM(units=50, return_sequences=True, input_shape=(X_train_scaled.shape[1], X_train_scaled.shape[2])))
model.add(Dropout(0.2))
model.add(LSTM(units=50, return_sequences=True))
model.add(Dropout(0.2))
model.add(Dense(units=1))  # Output layer for regression

# Compile the model
model.compile(optimizer='adam', loss='mean_squared_error')

# ===========================
# 6. Training the Model
# ===========================

# EarlyStopping to prevent overfitting
early_stopping = EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True)

# Start training timer
start_time = time.time()

# Train the model
history = model.fit(
    X_train_scaled, y_train_scaled,
    epochs=100,
    batch_size=32,  # Increased batch size for efficiency
    validation_split=0.2,
    callbacks=[early_stopping, time_callback],
    shuffle=True  # Shuffle for better generalization
)

# End training timer
end_time = time.time()
total_time = end_time - start_time
print(f"Total Training Time: {total_time:.2f} seconds")

# ===========================
# 7. Saving the Model and Scalers
# ===========================

# Save the trained model
model.save('cable_tension_lstm_model.h5')
print("Model saved to disk as 'cable_tension_lstm_model.h5'")

# Save the scalers
with open('scaler_features.pkl', 'wb') as f:
    pickle.dump(scaler_features, f)
with open('scaler_target.pkl', 'wb') as f:
    pickle.dump(scaler_target, f)
print("Scalers saved to disk as 'scaler_features.pkl' and 'scaler_target.pkl'")

# ===========================
# 8. Loading the Model and Scalers
# ===========================

# Load the model
loaded_model = load_model('cable_tension_lstm_model.h5')

# Load the scalers
with open('scaler_features.pkl', 'rb') as f:
    loaded_scaler_features = pickle.load(f)
with open('scaler_target.pkl', 'rb') as f:
    loaded_scaler_target = pickle.load(f)

# ===========================
# 9. Making Predictions
# ===========================

# Predict on the test set
y_pred_scaled = loaded_model.predict(X_test_scaled)

# Inverse transform the predictions and actual values to original scale
y_pred = loaded_scaler_target.inverse_transform(y_pred_scaled.reshape(-1, 1)).flatten()
y_test_original = loaded_scaler_target.inverse_transform(y_test_scaled.reshape(-1, 1)).flatten()

# ===========================
# 10. Evaluating the Model
# ===========================

# Calculate performance metrics
mse = mean_squared_error(y_test_original, y_pred)
rmse = np.sqrt(mse)
mae = mean_absolute_error(y_test_original, y_pred)
# Avoid division by zero in MAPE
non_zero = y_test_original != 0
mape = np.mean(np.abs((y_test_original[non_zero] - y_pred[non_zero]) / y_test_original[non_zero])) * 100
r2 = r2_score(y_test_original, y_pred)

print(f"Mean Squared Error (MSE): {mse:.4f}")
print(f"Root Mean Squared Error (RMSE): {rmse:.4f}")
print(f"Mean Absolute Error (MAE): {mae:.4f}")
print(f"Mean Absolute Percentage Error (MAPE): {mape:.2f}%")
print(f"R2 Score: {r2:.4f}")

# ===========================
# 11. Visualizing the Results
# ===========================

plt.figure(figsize=(12, 6))
plt.plot(y_test_original, label='Actual Cable Tension', alpha=0.7)
plt.plot(y_pred, label='Predicted Cable Tension', alpha=0.7)
plt.title('Actual vs Predicted Cable Tension')
plt.xlabel('Samples')
plt.ylabel('Cable Tension (kN)')
plt.legend()
plt.show()"