Exercise: Solving a Forward Problem with PiTGWFlow

Welcome to this exercise on using the Physics-Informed Transient Groundwater Flow model, PiTGWFlow, available in fusionlab-learn. This model is a Physics-Informed Neural Network (PINN) that learns by satisfying the governing physical equations rather than by fitting to labeled data.

We will perform a forward problem simulation. This means we will provide the model with known physical parameters and a domain, and its task will be to discover the hydraulic head field, \(h(t, x, y)\), that satisfies the groundwater flow PDE.

Learning Objectives:

  • Generate a set of “collocation points” within a defined spatio-temporal domain.

  • Understand how to configure PiTGWFlow with both fixed and learnable physical parameters.

  • Create a tf.data.Dataset suitable for a PINN, including the use of dummy targets for Keras API compatibility.

  • Define, compile, and train the PiTGWFlow model using its custom, physics-based training loop.

  • Visualize the training progress by plotting the PDE loss.

  • Visualize the final, continuous solution learned by the network.

Let’s get started!

Prerequisites

Ensure you have fusionlab-learn and its common dependencies installed. For visualizations, matplotlib is also needed.

pip install fusionlab-learn matplotlib

Step 1: Imports and Setup

First, we import all necessary libraries and set up our environment for reproducibility and cleaner output.

 1import os
 2import numpy as np
 3import tensorflow as tf
 4import matplotlib.pyplot as plt
 5import warnings
 6
 7# FusionLab imports
 8from fusionlab.nn.pinn import PiTGWFlow
 9from fusionlab.params import LearnableK
10
11# Suppress warnings and TF logs for cleaner output
12warnings.filterwarnings('ignore')
13tf.get_logger().setLevel('ERROR')
14os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
15
16# Directory for saving any output images from this exercise
17EXERCISE_OUTPUT_DIR = "./pitgwflow_exercise_outputs"
18os.makedirs(EXERCISE_OUTPUT_DIR, exist_ok=True)
19
20print("Libraries imported and setup complete for PiTGWFlow exercise.")

Expected Output:

Libraries imported and setup complete for PiTGWFlow exercise.

Step 2: Generate Collocation Points

Unlike traditional models, PINNs are not trained on labeled data. Instead, they are trained on “collocation points”—randomly sampled points in the time and space domain where the governing PDE is enforced.

 1N_POINTS = 5000
 2SEED = 42
 3tf.random.set_seed(SEED)
 4
 5# Define the domain boundaries [t_min, t_max], [x_min, x_max], etc.
 6t_bounds, x_bounds, y_bounds = [0., 1.], [-1., 1.], [-1., 1.]
 7
 8# Generate random points within the domain
 9coords = {
10    't': tf.random.uniform((N_POINTS, 1), *t_bounds),
11    'x': tf.random.uniform((N_POINTS, 1), *x_bounds),
12    'y': tf.random.uniform((N_POINTS, 1), *y_bounds),
13}
14
15print(f"Generated {N_POINTS} random collocation points.")
16print(f"Shape of 't' tensor: {coords['t'].shape}")

Expected Output:

Generated 5000 random collocation points.
Shape of 't' tensor: (5000, 1)

Step 3: Prepare the Dataset for Training

We package our collocation points into a tf.data.Dataset for efficient training. For compatibility with the standard Keras .fit() API, we must provide a “dummy” target tensor. This target is completely ignored by PiTGWFlow’s custom training logic, as the loss is calculated from the PDE residual, not from a data-driven error.

 1BATCH_SIZE = 128
 2
 3# Create dummy targets (an array of zeros)
 4dummy_targets = tf.zeros_like(coords['t'])
 5
 6# Create the dataset
 7dataset = tf.data.Dataset.from_tensor_slices(
 8    (coords, dummy_targets)
 9).shuffle(buffer_size=N_POINTS).batch(BATCH_SIZE)
10
11print(f"Dataset created with batch size {BATCH_SIZE}.")
12print(f"Dataset element spec: {dataset.element_spec}")

Expected Output:

Dataset created with batch size 128.
Dataset element spec: ({'t': TensorSpec(shape=(None, 1), dtype=tf.float32, name=None), 'x': TensorSpec(shape=(None, 1), dtype=tf.float32, name=None), 'y': TensorSpec(shape=(None, 1), dtype=tf.float32, name=None)}, TensorSpec(shape=(None, 1), dtype=tf.float32, name=None))

Step 4: Define, Compile, and Train PiTGWFlow

Now we instantiate PiTGWFlow. We will set most physical parameters as fixed constants but define hydraulic conductivity \(K\) as a LearnableK object. This demonstrates how the model can be used to infer physical parameters. We then compile and train the model.

 1# Instantiate the PINN model
 2pinn_model = PiTGWFlow(
 3    hidden_units=[50, 50, 50],
 4    activation='tanh',
 5    K=LearnableK(initial_value=0.5), # Start with a guess for K
 6    Ss=1e-4,                         # This is a fixed value
 7    Q=0.1                            # A constant source term
 8)
 9
10# Compile the model (no loss needed, it's handled internally)
11pinn_model.compile()
12
13# Train the model
14print("\nStarting PiTGWFlow model training...")
15history = pinn_model.fit(
16    dataset,
17    epochs=20,
18    verbose=1
19)
20print("Training complete.")

Expected Output:

Starting PiTGWFlow model training...
Epoch 1/20
40/40 [==============================] - 3s 4ms/step - pde_loss: 0.0125
Epoch 2/20
40/40 [==============================] - 0s 4ms/step - pde_loss: 5.123e-04
...
Epoch 20/20
40/40 [==============================] - 0s 4ms/step - pde_loss: 8.910e-06
Training complete.

Step 5: Visualize Training History

We can plot the pde_loss from the training history to confirm that the model successfully learned to minimize the PDE residual. A log scale on the y-axis is helpful to see the rapid decrease in loss.

 1print("\nPlotting training history...")
 2plt.figure(figsize=(10, 6))
 3plt.plot(history.history['pde_loss'], label='PDE Loss')
 4plt.yscale('log')
 5plt.title('PiTGWFlow Training History')
 6plt.xlabel('Epoch')
 7plt.ylabel('Log PDE Loss')
 8plt.legend()
 9plt.grid(True, which="both", ls="--")
10fig_path = os.path.join(EXERCISE_OUTPUT_DIR, "pitgwflow_exercise_loss.png")
11plt.savefig(fig_path)
12plt.show()

Example Output Plot:

PiTGWFlow Training History Plot

An example plot showing the PDE loss decreasing over epochs. This demonstrates that the neural network is successfully learning a solution that conforms to the governing physics.

Step 6: Visualize the Learned Solution

The great advantage of a PINN is that it represents a continuous solution. We can evaluate the trained model on a regular grid of points to visualize the hydraulic head field \(h(t, x, y)\) at a specific moment in time.

 1# Create a meshgrid for visualization at a specific time t
 2t_slice = 0.5
 3x_range = np.linspace(x_bounds[0], x_bounds[1], 100)
 4y_range = np.linspace(y_bounds[0], y_bounds[1], 100)
 5X, Y = np.meshgrid(x_range, y_range)
 6
 7# --- FIX: Prepare grid points for prediction ---
 8# The model expects a batch of points, not a grid. We need to
 9# flatten the X and Y grids into a list of (x, y) coordinates.
10# The shape for each input tensor must be (N, 1).
11x_flat = tf.convert_to_tensor(X.ravel(), dtype=tf.float32)
12y_flat = tf.convert_to_tensor(Y.ravel(), dtype=tf.float32)
13
14# Create the corresponding 't' tensor for each point
15t_flat = tf.fill(x_flat.shape, t_slice)
16
17# Reshape all to be column vectors (N, 1)
18grid_coords = {
19    't': tf.reshape(t_flat, (-1, 1)),
20    'x': tf.reshape(x_flat, (-1, 1)),
21    'y': tf.reshape(y_flat, (-1, 1))
22}
23
24# Predict the hydraulic head 'h' on the flattened grid
25h_pred_flat = pinn_model.predict(grid_coords)
26
27# --- FIX: Reshape the flat predictions back to the grid shape ---
28# The output prediction will be flat, so we reshape it to the
29# original grid's shape (100x100) for plotting with contourf.
30h_pred_grid = tf.reshape(h_pred_flat, X.shape)
31
32# Plot the contour of the solution
33plt.figure(figsize=(9, 7))
34contour = plt.contourf(X, Y, h_pred_grid, 100, cmap='viridis')
35plt.colorbar(contour, label='Hydraulic Head (h)')
36plt.title(f'Learned Hydraulic Head Solution at t = {t_slice}')
37plt.xlabel('x-coordinate')
38plt.ylabel('y-coordinate')
39plt.axis('equal')
40fig_path = os.path.join(EXERCISE_OUTPUT_DIR, "pitgwflow_exercise_solution.png")
41plt.savefig(fig_path)
42plt.show()
43
44# Or  use the  utility function to easily visualize the result.
45
46# from fusionlab.nn.pinn.utils import plot_hydraulic_head
47
48# plot_hydraulic_head(
49#    model=pinn_model,
50#    t_slice=0.5,
51#    x_bounds=(-1.0, 1.0),
52#    y_bounds=(-1.0, 1.0),
53#    resolution=100,
54#    save_path=os.path.join(EXERCISE_OUTPUT_DIR, "pitgwflow_exercise_solution.png"),
55#    show_plot=True
56# )

Expected Plot:

PiTGWFlow Learned Solution

Visualization of the continuous hydraulic head field \(h(x, y)\) at a fixed time, as learned by the PiTGWFlow model. The plot shows the model’s ability to generate a complete solution over the entire domain.

Discussion of Exercise

Congratulations! In this exercise, you have successfully used the PiTGWFlow model to solve a forward physics problem:

  • You correctly generated collocation points to define the problem domain instead of using labeled data.

  • You prepared a tf.data.Dataset compatible with the Keras API for an unsupervised, physics-driven task.

  • You instantiated, trained, and evaluated the PiTGWFlow model, observing the decrease in the physics-based PDE loss.

  • You visualized the final output, demonstrating that the model learned a continuous solution to the governing equation across the entire domain.

This exercise provides a solid foundation for using PINNs to tackle more complex scientific and engineering problems.