HydroTuner: Usage Examples

This page provides practical, end-to-end examples for using the HydroTuner to find optimal hyperparameters for the library’s hydrogeological PINN models.

The core workflow involves three main steps: 1. Preparing your data as NumPy arrays. 2. Defining a search_space dictionary that specifies which

hyperparameters to tune.

  1. Using the HydroTuner.create() factory method to instantiate and run the tuner.

We will start with a comprehensive workflow for the fully-coupled TransFlowSubsNet model.


Example 1: A Comprehensive Workflow for Tuning TransFlowSubsNet

In this example, we’ll configure the HydroTuner to find the best hyperparameters for a TransFlowSubsNet model. Our search space will be comprehensive, including architectural parameters for the data-driven core, physical parameters for the PINN module, and optimization parameters for the training process.

Step 1: Imports and Data Generation

First, we set up our environment and generate a synthetic dataset. This dataset mimics a real-world scenario by including all the data types that TransFlowSubsNet is designed to handle.

 1import os
 2import numpy as np
 3import tensorflow as tf
 4
 5# FusionLab imports
 6from fusionlab.nn.forecast_tuner import HydroTuner
 7from fusionlab.nn.pinn.models import TransFlowSubsNet
 8
 9# --- Configuration ---
10N_SAMPLES, T_PAST, HORIZON = 500, 15, 7
11S_DIM, D_DIM, F_DIM = 4, 6, 3
12RUN_DIR = "./hydrotuner_examples"
13
14# --- Generate Dummy Data as NumPy Arrays ---
15print("Generating synthetic data for tuning...")
16inputs = {
17    # Coords (t,x,y) for the physics loss calculation
18    "coords": np.random.rand(N_SAMPLES, HORIZON, 3).astype(np.float32),
19    # Time-invariant features
20    "static_features": np.random.rand(N_SAMPLES, S_DIM).astype(np.float32),
21    # Historical time-varying features
22    "dynamic_features": np.random.rand(N_SAMPLES, T_PAST, D_DIM).astype(np.float32),
23    # Known future time-varying features
24    "future_features": np.random.rand(N_SAMPLES, HORIZON, F_DIM).astype(np.float32),
25}
26targets = {
27    # The two target variables to forecast
28    "subsidence": np.random.rand(N_SAMPLES, HORIZON, 1).astype(np.float32),
29    "gwl": np.random.rand(N_SAMPLES, HORIZON, 1).astype(np.float32)
30}
31print(f"Generated {N_SAMPLES} data samples.")

Step 2: Define the Hyperparameter Search Space

This dictionary is the core of our tuning experiment. It tells the HydroTuner exactly which parameters to optimize and what values or ranges to explore for each one. We define three types of hyperparameters:

  • Architectural HPs: Control the size and capacity of the

    data-driven BaseAttentive core.

  • Physics HPs: Control the physical coefficients in the PDEs,

    allowing the tuner to test fixed vs. learnable assumptions.

  • Compile-time HPs: Control the optimization process, including

    the learning rate and the weights of the physics loss terms.

 1transflow_search_space = {
 2    # --- Architectural Hyperparameters ---
 3    "embed_dim": [32, 64],
 4    "attention_units": [32, 64],
 5    "num_heads": [2, 4],
 6    "dropout_rate": {"type": "float", "min_value": 0.05, "max_value": 0.3},
 7
 8    # --- Physics Hyperparameters for TransFlowSubsNet ---
 9    # Test if making K learnable is better than two fixed values
10    "K": ["learnable", 1e-5, 1e-4],
11    # Search for the best Ss value in a log space
12    "Ss": {"type": "float", "min_value": 1e-6, "max_value": 1e-4, "sampling": "log"},
13    # For this experiment, we'll fix C as learnable
14    "pinn_coefficient_C": ["learnable"],
15
16    # --- Compile-time Hyperparameters ---
17    "learning_rate": {"type": "choice", "values": [1e-3, 5e-4, 1e-4]},
18    # Search for the best weights for the two physics losses
19    "lambda_gw": {"type": "float", "min_value": 0.5, "max_value": 1.5},
20    "lambda_cons": {"type": "float", "min_value": 0.1, "max_value": 1.0}
21}
22print("Hyperparameter search space for TransFlowSubsNet defined.")

Step 3: Create and Run the Tuner

We use the HydroTuner.create() factory method. This is the recommended approach as it simplifies setup by automatically inspecting our data to determine the fixed parameters (like input/output dimensions). We then call the high-level .run() method, which handles all the underlying data conversion and launches the Keras Tuner search.

 1# 1. Create the tuner instance using the factory method
 2tuner = HydroTuner.create(
 3    model_name_or_cls=TransFlowSubsNet,
 4    inputs_data=inputs,
 5    targets_data=targets,
 6    search_space=transflow_search_space,
 7    # Keras Tuner configuration
 8    max_trials=5, # Keep low for this example; use 30-50 for real tasks
 9    project_name="TransFlowSubsNet_Comprehensive_Tuning",
10    directory=RUN_DIR,
11    overwrite=True,
12    objective="val_loss" # Monitor the total validation loss
13)
14
15# 2. Run the search process
16print("\nStarting hyperparameter search for TransFlowSubsNet...")
17best_model, best_hps, tuner_instance = tuner.run(
18    inputs=inputs,
19    y=targets,
20    validation_data=(inputs, targets), # Use same data for example
21    epochs=5, # Train each trial for a few epochs
22    batch_size=64,
23    callbacks=[tf.keras.callbacks.EarlyStopping('val_loss', patience=3)]
24)

Step 4: Analyze Results and Use the Best Model

After the search is complete, the tuner object provides access to the best hyperparameters and the best model, which has been automatically retrained on the full dataset for you.

 1print("\n--- Hyperparameter Search Complete ---")
 2
 3if best_hps:
 4    print("\nBest Hyperparameters Found:")
 5    for hp, value in best_hps.values.items():
 6        # Format floats for readability
 7        if isinstance(value, float):
 8            print(f"  - {hp}: {value:.5f}")
 9        else:
10            print(f"  - {hp}: {value}")
11
12    # The best_model is ready for prediction or saving
13    # best_model.save(os.path.join(RUN_DIR, "best_transflow_model.keras"))
14    # print("\nBest model saved.")
15else:
16    print("Search finished, but no best hyperparameters were found.")

Expected Output:

Starting hyperparameter search for TransFlowSubsNet...
Trial 5 Complete [00h 00m 45s]
val_loss: 0.168...

Results summary
[...]
--- Hyperparameter Search Complete ---

Best Hyperparameters Found:
  - embed_dim: 32
  - attention_units: 64
  - num_heads: 4
  - dropout_rate: 0.17581
  - K: learnable
  - Ss: 0.00003
  - pinn_coefficient_C: learnable
  - learning_rate: 0.00100
  - lambda_gw: 1.25678
  - lambda_cons: 0.45901

Example 2: Tuning PIHALNet

This example showcases the power and flexibility of the HydroTuner. We will now tune the PIHALNet model, which has a different set of physical parameters than TransFlowSubsNet.

The core workflow remains identical. We will reuse the same dataset from Example 1, but we will define a new search_space tailored specifically to the hyperparameters available in PIHALNet.

Step 1: Define a PIHALNet-Specific Search Space

The key to adapting the tuner is creating a search_space that matches the target model. For PIHALNet, we are interested in tuning its unique consolidation coefficient (\(C\)) and its single physics loss weight (\(\lambda_{physics}\)).

Note that we omit the hyperparameters for groundwater flow (K, Ss, lambda_gw), as they are not relevant to the PIHALNet model.

 1pihalnet_search_space = {
 2    # --- Architectural Hyperparameters (can be different) ---
 3    # For this run, let's fix the embedding dimension and explore others.
 4    "embed_dim": [32],
 5    "num_heads": [4, 8],
 6    "dropout_rate": {"type": "float", "min_value": 0.1, "max_value": 0.5},
 7
 8    # --- Physics Hyperparameters for PIHALNet ---
 9    # Test a few different fixed values for the consolidation coefficient C
10    "pinn_coefficient_C": [1e-3, 5e-3, 1e-2],
11
12    # --- Compile-time Hyperparameters ---
13    "learning_rate": [1e-3, 5e-4],
14    # PIHALNet uses a single physics weight, which we name `lambda_physics`
15    # Note: The tuner's `build` method will correctly pass this to the
16    # model's `compile` method, which may expect a different name
17    # like `lambda_pde`. This mapping is handled internally.
18    "lambda_physics": {"type": "float", "min_value": 0.1, "max_value": 1.0}
19}
20print("Defined a new search space tailored for PIHALNet.")

Step 2: Create and Run the Tuner for PIHALNet

The process is exactly the same as before. The only changes are in the arguments passed to HydroTuner.create(): we now specify model_name_or_cls=PIHALNet and pass our new pihalnet_search_space.

 1from fusionlab.nn.pinn.models import PIHALNet
 2from tensorflow.keras.callbacks import EarlyStopping
 3
 4# 1. Create the tuner instance for PIHALNet
 5tuner_pihal = HydroTuner.create(
 6    model_name_or_cls=PIHALNet, # <-- The primary change
 7    inputs_data=inputs,
 8    targets_data=targets,
 9    search_space=pihalnet_search_space, # <-- Use the new search space
10    max_trials=3,
11    project_name="PIHALNet_Example_Tuning",
12    directory=RUN_DIR,
13    overwrite=True
14)
15
16# 2. Run the search
17print("\nStarting tuning for PIHALNet...")
18best_model_pihal, best_hps_pihal, _ = tuner_pihal.run(
19    inputs=inputs,
20    y=targets,
21    validation_data=(inputs, targets),
22    epochs=3,
23    batch_size=64,
24    callbacks=[EarlyStopping('val_loss', patience=2)]
25)

Step 3: Analyze the PIHALNet Results

Finally, we inspect the output to see the best combination of hyperparameters that the tuner discovered for the PIHALNet model.

1print("\n--- Best Hyperparameters for PIHALNet ---")
2if best_hps_pihal:
3    for hp, value in best_hps_pihal.values.items():
4        if isinstance(value, float):
5            print(f"  - {hp}: {value:.5f}")
6        else:
7            print(f"  - {hp}: {value}")
8else:
9    print("Search finished, but no best hyperparameters were found.")

Expected Output:

--- Best Hyperparameters for PIHALNet ---
  - embed_dim: 32
  - num_heads: 4
  - dropout_rate: 0.25123
  - pinn_coefficient_C: 0.00500
  - learning_rate: 0.00100
  - lambda_physics: 0.67890

These examples illustrate the power of the HydroTuner’s generic design. The core workflow remains consistent, and you can easily adapt it to different models by providing the appropriate model class and defining a relevant search_space. This dramatically accelerates the process of experimentation and optimization.