Here is a simple code I wrote for Data-less đ Physics Informed Neural đ§ Network đ¸️ to solve the steady-state 2D incompressible Navier-Stokes equations in Python using PyTorch. Already validated results will be uploaded here in good time. The code has all the comments đ¨đĢ you could possibly need. If you have questions, well... đš
Please note that a properly cooled đ§ GPU is recommended for running this code. If your CPU melts đĨ, don't blame me đ¤. Furthermore, if you classify yourself as a poor peasant đ¨đž, use: device = torch.device("cpu") # cuda for gpu, cpu for cpu đ.
The New Code: (Fall 2025)
After successfully đ completing the validation for the SIMB, yours truly has been developing in the abundant spare time đ; it is now the perfect time to revisit PINNs. On a fine morning, yours truly decided to separate đ¯đđ§♡ the tensors for boundary conditions and the equations of motion. It turns out đą that making separate functions for the boundary conditions and the equations of motion with separate tensors īŽŠŲ¨ŲīŽŠīŽŠŲ¨Ų♡īŽŠŲ¨ŲīŽŠīŽŠŲ¨Ų for each boundary makes the loss function converge to the global minimum đŗ️ much faster ⚡.
Copyright <2025> <Fahad Butt>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
The helper function to define Navier-Stokes equations is defined first, navier_stokes_equation. This function calculates the the model output at interior points and then calculates the derivatives using autograd to assemble the equations of motion.
The boundary condition helper function is defined next, boundary_conditions. This function evaluates the model output at each boundary separately and then enforces the boundary conditions. As a result of these modifications đ ️, the loss đ is also rewritten.
def navier_stokes_equation(ns):
ns.requires_grad_(True) # watch ns for gradient computation
out = model(ns) # output from model at interior
u = out[:, 0] # u interior
v = out[:, 1] # v interior
p = out[:, 2] # p interior
du = torch.autograd.grad(u, ns, torch.ones_like(u), create_graph=True)[0] # du/dns
dv = torch.autograd.grad(v, ns, torch.ones_like(v), create_graph=True)[0] # dv/dns
nn.MSELoss()(v_bottom_bc, torch.zeros_like(v_bottom_bc)) # v momentum boundary condition loss
bc_loss = u_bc_loss + v_bc_loss # total boundary condition loss
mse_loss = x_mom_loss + y_mom_loss + cont_loss # total equation loss
total_loss = mse_loss + bc_loss # total loss
return total_loss
Instead of creating a single tensor đ§ for the entire input, five đ§Ž tensors are created for this new, at least to yours truly, method. Of course, the training loop also has to be modified đŠ.
B_left = torch.tensor(cp.vstack((x_l, y_l)).T, dtype=torch.float32).to(device) # stack x, y together
optimizer.zero_grad() # clear gradients of all optimized variables
loss_value = loss_fn() # compute prediction by passing inputs to model
loss_value.backward() # compute gradient of loss with respect to model parameters
optimizer.step() # perform parameter up date
The Code:
#%% license
#Copyright <2024> <Fahad Butt>
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#%% import necessary libraries
import os
import numpy as np
import random as rn
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
# import torch_directml # enable for any gpu except CUDA gpu
import glob
device = torch.device("cpu") # cuda for CUDA gpu, cpu for cpu, torch_directml.device() for any other gpu
#%% 2D navier-stokes equations and boundary conditions
def navier_stokes_equation(ns):
ns.requires_grad_(True) # watch ns for gradient computation
out = model(ns) # output from model
u = out[:, 0] # x velocity
v = out[:, 1] # y velocity
p = out[:, 2] # pressure
du = torch.autograd.grad(u, ns, torch.ones_like(u), create_graph=True)[0] # du/dns
dv = torch.autograd.grad(v, ns, torch.ones_like(v), create_graph=True)[0] # dv/dns
num_trainable_params = count_parameters(model) # show trainable parameters
print("Number of trainable parameters:", num_trainable_params)
#%% prepare input data
train_points = np.vstack((x, y)).T # stack x, y together
train_points = torch.tensor(train_points, dtype=torch.float32).to(device) # convert to tensor and move training points to GPU / CPU, use float64 if mroe precision is required
#%% train using Adam
resume_training = False # True if resuming training
start_epoch = 1
optimizer = optim.Adam(model.parameters(), lr = 0.00001) # select optimizer and parameters
checkpoint_dir = './pytorch_checkpoints' # directory to save checkpoints
os.makedirs(checkpoint_dir, exist_ok=True) # create a directory to save checkpoints
x_m_te = np.random.uniform(0, L, 50000) # mid field
y_m_te = np.random.uniform(0, D, 50000)
x_te = np.concatenate([x_l_te, x_r_te, x_b_te, x_t_te, x_m_te]) # x co-ordinates
y_te = np.concatenate([y_l_te, y_r_te, y_b_te, y_t_te, y_m_te]) # y co-ordinates
test_points = np.vstack((x_te, y_te)).T # stack x, y together
test_points = torch.tensor(test_points, dtype=torch.float32).to(device) # convert to tensor and move training points to GPU / CPU, use float64 if mroe precision is required
predict = model(test_points).detach().cpu().numpy() # predict and move back to CPU
u = predict[:, 0] * Uinf # predict and bring back to dimensional form
sc = plt.scatter(x_te, y_te, c = u, cmap = 'jet', s = 1, edgecolor = 'none', alpha = 1)
plt.colorbar(orientation = 'vertical')
plt.gca().set_aspect('equal', adjustable = 'box')
plt.xticks([0, L])
plt.yticks([0, D])
plt.xlabel('x [m]')
plt.ylabel('y [m]')
plt.axis('off')
plt.show()
plt.figure(dpi = 300)
sc = plt.scatter(x_te, y_te, c = v, cmap = 'jet', s = 1, edgecolor = 'none', alpha = 1)
plt.colorbar(orientation = 'vertical')
plt.gca().set_aspect('equal', adjustable = 'box')
plt.xticks([0, L])
plt.yticks([0, D])
plt.xlabel('x [m]')
plt.ylabel('y [m]')
plt.axis('off')
plt.show()
plt.figure(dpi = 300)
sc = plt.scatter(x_te, y_te, c = p, cmap = 'jet', s = 1, edgecolor = 'none', alpha = 1)
plt.colorbar(orientation = 'vertical')
plt.gca().set_aspect('equal', adjustable = 'box')
plt.xticks([0, L])
plt.yticks([0, D])
plt.xlabel('x [m]')
plt.ylabel('y [m]')
plt.axis('off')
plt.show()
Flow between Flat - Plates
The boundary conditions are taken from [2]. The results presented in Fig. 1 are from flow between two parallel flat plates. For these boundary conditions, analytical solutions to the Navier-Stokes equations are available. As this is "just another blog" đ; just by eye-balling đ, the results look very promising. I mean from Fig. 1, the velocities at left side are captured well-ish!
Fig. 1, post processing
Lid-Driven Cavity
The case of flow between two flat plates shown in Fig. 1 is self-validating as area times velocity should be same for inlet and outlet, which it is. Lid-Driven Cavity however, is a benchmark problem for many numerical methods in fluid dynamics because of complex vortex dynamics. Here I present, the results of Lid-Driven Cavity problem. The results are very good đ. The results are presented in Fig. 2. The comparison of PINN results with [1] is shown in Fig. 3. The validation of CFD is available to be read here. While the CFD code is available here.
Fig. 2, Post processing
Fig. 3, A comparison
External Fluid Dynamics
Here is a video of the PINN learning flow around obstacle i.e. a square cylinder.
Fig. 4, The animation
The PINN is now capable to predict flow around curved objects. The flow around circular cylinder is mentioned. I use polar coordinates for applying Neuman boundary conditions for pressure to satisfy the no-slip wall. The results are shown in Fig. 5. The code is also made available for fellow researchers to improve upon and use in the research, please cite properly. The results show u and v velocities at the left portion while pressure and streamlines are shown on the right side. As compared to the commercial software that shall not be named đ, the results are very accurate. The Reynolds number is kept at 10to avoid making the loss landscape complex. Please refer to this post for more details!
Fig. 5, The post processing
Code
# Copyright <2024> <FAHAD BUTT>
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#%% import necessary libraries
import os
import numpy as np
import random as rn
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.init as init
import torch.optim as optim
# import torch_directml # enable for AMD and Intel gpu
import glob
from scipy.interpolate import griddata
device = torch.device("cpu") # cuda for CUDA gpu, cpu for cpu, mps for Apple GPU, torch_directml.device() for any other gpu
#%% 2D navier-stokes equations and boundary conditions
def navier_stokes_equation(ns):
ns.requires_grad_(True) # watch ns for gradient computation
out = model(ns) # output from model
u = out[:, 0] # x velocity
v = out[:, 1] # y velocity
p = out[:, 2] # pressure
du = torch.autograd.grad(u, ns, torch.ones_like(u), create_graph = True)[0] # du/dns
dv = torch.autograd.grad(v, ns, torch.ones_like(v), create_graph = True)[0] # dv/dns
num_trainable_params = count_parameters(model) # show trainable parameters
print("Number of trainable parameters:", num_trainable_params)
#%% prepare input data
train_points = np.vstack((x_train, y_train)).T # stack x, y together
train_points = torch.tensor(train_points, dtype=torch.float32).to(device) # convert to tensor and move training points to GPU / CPU, use float64 if more precision is required
#%% train using Adam
resume_training = False # True if resuming training
start_epoch = 1
optimizer = optim.Adam(model.parameters(), lr = 0.0001) # select optimizer and parameters
checkpoint_dir = './pytorch_checkpoints' # directory to save checkpoints
os.makedirs(checkpoint_dir, exist_ok = True) # create a directory to save checkpoints
x_te = np.concatenate([x_l_te, x_r_te, x_b_te, x_t_te, x_m_te]) # x co-ordinates
y_te = np.concatenate([y_l_te, y_r_te, y_b_te, y_t_te, y_m_te]) # y co-ordinates
distance_te = np.sqrt((x_te - center_x) ** 2 + (y_te - center_y) ** 2) # distance of each point from center
outside_circle_te = distance_te >= radius # filter out points inside circle
x_te_filtered = x_te[outside_circle_te]
y_te_filtered = y_te[outside_circle_te]
test_points = np.vstack((x_te_filtered, y_te_filtered)).T # stack x, y together
test_points = torch.tensor(test_points, dtype=torch.float32).to(device) # convert to tensor and move training points to GPU / CPU, use float64 if more precision is required
predict = model(test_points).detach().cpu().numpy() # predict and move back to CPU
u = predict[:, 0] * Uinf # predict and bring back to dimensional form
Thank you for reading! If you want to hire me as your next PostDoc researcher, reach out for collaboration in research, please do so!
References
[1] U. Ghia, K. N. Ghia, and C. T. Shin, “High-Re Solutions for Incompressible Flow Using the Navier-Stokes Equations and a Multigrid Method”, Journal of Computational Physics 48, 387-411 (1982)
[2] Cahya Amalinadhi, Pramudita S. Palar, Rafael Stevenson and Lavi Zuhal. "On Physics-Informed Deep Learning for Solving Navier-Stokes Equations," AIAA 2022-1436. AIAA SCITECH 2022 Forum. January 2022. https://doi.org/10.2514/6.2022-1436
Here I share đģ Data-less Physics-Informed Neural đ§ Network code written in Python đ using TensorFlow đ§Ž Keras. In the current configuration, the code will try to learn flow through a pipe. The output at various time instances is shown in Fig. 1. To increase neural network's capacity to learn complex features, like vortex dominated flows increase neurons in the hidden layers. To increase neural network's capacity to learn flow physics like boundary layer mechanics, increase the hidden layers. This is as far as I have understood via self-learning. Nice little alien đŊ technology this đ.
Please note that a properly cooled đ§ GPU is recommended for running this code. If your CPU melts đĨ, don't blame me đ¤.
MIT License
Copyright (c) 2024 Fahad Butt
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Code
#%% import stuff
import os
import random as rn
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.layers.experimental import preprocessing
train_points_x, train_points_y, train_points_t = np.meshgrid(x, y, t) # create combinations of sample points along the domain
train_points = np.vstack((train_points_x.flatten(), train_points_y.flatten(), train_points_t.flatten())).T # flatten the grid to feed to neural network
model.layers[0].adapt(train_points) # set up the normalization layer
optimizer = tf.keras.optimizers.Adam(learning_rate=0.01) # specify learning rate and the optimizer
checkpoint_dir = './checkpoints' # check point directory (same as script)
split('-')[-1]) # determine the starting epoch based on the loaded checkpoint
max_epochs = 20000 # maximum iterations
epoch = 0 # iteration counter
loss_value = float('inf') # initial loss
min_loss = 1e-30 # minimum loss (any acceptable value)
#%% Train the neural network
while epoch <= max_epochs and loss_value >= min_loss:
with tf.GradientTape() as tape: # use tf.GradientTape to record operations for automatic differentiation
loss_value = navier_stokes_loss(train_points) # compute total loss by evaluating Navier-Stokes loss function on training points
gradients = tape.gradient(loss_value, model.trainable_variables) # calculate gradients of total loss with respect to trainable variables of model
optimizer.apply_gradients(zip(gradients, model.trainable_variables)) # update model trainable variables using calculated gradients and specified optimizer
epoch += 1 # increases iteration counter by 1
if epoch % 100 == 0:
print(f"Epoch {epoch}, Loss: {loss_value.numpy()}") # output loss while training
save_checkpoint() # save training progress for resume
#%% predicting and plotting
target_time_step = TT # in seconds
x_t = np.linspace(0, L, 50) # initialize x
y_t = np.linspace(0, D, 50) # initialize y
test_points_x, test_points_y = np.meshgrid(x_t, y_t) # create pairs of sample points along the domain (for testing)
test_points = np.vstack((test_points_x.flatten(), test_points_y.flatten())).T # flatten the grid to feed to neural network model
test_points_t = np.column_stack([test_points, np.full_like(test_points[:, 0], target_time_step)]) # generate test points for the target time step
# predict velocity components and pressure at target time step
u_predicted_t = model.predict(test_points_t)[:, 0] # predict u velocity
v_predicted_t = model.predict(test_points_t)[:, 1] # predict v velocity
I tried to add as many comments in the code as possible. If you still can't understand, open a book đ? Thank you for reading! If you want to hide me as your next PhD student, please reach out!
Here is code to solve 2D Laplace 𧨠equation using PINNđ§ . This đŊ technology works with/without previous data. Soon I will add data loss using finite difference so to improve the neural network's accuracy. Soon đ¤Ŗ. To convert this to 3D, add T_z and T_zz terms in laplace_equation and loss_fn functions.
import random as rn
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from tensorflow.keras.layers.experimental import preprocessing
# set same seeds for initialization
np.random.seed(0) # same random initializations every time numpy
rn.seed(0) # same random initializations every time for python
tf.random.set_seed(0) # same random initializations every time for TensorFlow
# define the physics informed neural network structure
model = keras.Sequential([ # define a feedforward neural network with backpropagation and gradient descent with ADAM
preprocessing.Normalization(input_shape=(2,)), # normalize inputs (2 for 2D, 3 for 3D)
layers.Dense(50, activation='tanh', input_shape=(2, ), kernel_initializer='zeros'), # first hidden layer
layers.Dense(50, activation='tanh'), # second hidden layer
layers.Dense(1, activation=None) # output layer
])
# define the 2D Laplace equation and boundary conditions
def laplace_equation(le):
with tf.GradientTape(persistent=True) as tape:
tape.watch(le) # set the input variable le for gradient computation
T = model(le) # predict the temperature field T using the neural network model
T_x = tape.gradient(T, le)[:, 0] # first derivative of neural network output with respect to x
T_y = tape.gradient(T, le)[:, 1] # first derivative of neural network output with respect to y
T_xx = tape.gradient(T_x, le)[:, 0] # second derivative of neural network output with respect to x
T_yy = tape.gradient(T_y, le)[:, 1] # second derivative of neural network output with respect to y
plt.contourf(test_points_x, test_points_y, predicted_temperature, cmap='turbo', levels=256) # contours of temperature
plt.colorbar(label='Temperature')
plt.xlabel('Length [m]')
plt.ylabel('Width [m]')
plt.title('2D Laplace Equation Solution using PINN')
plt.clim(0, 20) # Set legend limits
plt.show() # make plot
Fig. 1, shows comparison with FDM using both Neumann and Dirichlet boundary conditions. To apply Neumann boundary conditions, simply put T_x as 0 instead of T in the laplace_equation function. Within Fig. 1, right side is Dirichlet while Neumann is on the right side. Personally, FVM is still the king đ.
Fig. 1, Top row PINN bottom row FDM
Update 01
This new code has functions and handles the normalization of inputs manually. This version also has less derivatives to compute by some trickery, so it runs faster. Navier-Stokes version soon! I am not sure if PINN for Navier-Stokes equations go in CFD blog or in this blog? đ Also, this Update 1 code uses Keras 3 which is available on tensorflow-2.16.1.
#%% import stuff
import random as rn
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
import keras
from keras import layers
#%% define the 2D Laplace equation and boundary conditions
def laplace_equation(le):
with tf.GradientTape(persistent=True) as tape2:
tape2.watch(le) # watch the input variable le for 2nd order gradient computation
with tf.GradientTape(persistent=True) as tape1:
tape1.watch(le) # set the input variable le for 1st order gradient computation
T = model(le) # predict the temperature field T using the neural network model
layers.Dense(1, activation = None)]) # output layer, 1 for T
model.summary() # display model
#%% define the physics informed neural network structure
train_points_x, train_points_y = np.meshgrid(x, y) # create pairs of sample points along the domain
train_points = np.vstack((train_points_x.flatten(), train_points_y.flatten())).T # flatten the grid to feed to neural network
optimizer = tf.keras.optimizers.Adam() # specify learning rate and optimizer
max_epochs = 10000 # maximum iterations
for epoch in range(1, max_epochs + 1): # training loop
with tf.GradientTape() as tape3:
loss_value = loss_fn(train_points) # compute total loss by evaluating Navier-Stokes loss function on training points
gradients = tape3.gradient(loss_value, model.trainable_variables) # calculate gradients of total loss with respect to trainable variables of model
optimizer.apply_gradients(zip(gradients, model.trainable_variables)) # update model trainable variables using calculated gradients and specified optimizer
Thank you for reading! If you want to hire me as you brand new PhD student or want to collaborate on research, please reach out!
Sunday, January 14, 2024
After successfully creating my own neural network from scratch using math and calculus only [here], I have taught myself to do the same in TensorFlow using CPU only. So one can run this without CUDA GPU. I have added comments; if the readers are like me and migrating from MATLAB; the comments will help, probably đ. The results are shown in Fig. 1.
# import libraries
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras import layers, models, optimizers, callbacks
# generate data
x = np.linspace(0, 1 * np.pi, round((1 * np.pi) / (np.pi / 16))).reshape(-1, 1) # reshape is like x' in MATLAB, create neural network inputs from 0 to pi, with spacing of pi/16
y = x**2 * np.sin(x) # the function we want neural network to learn
# create neural network
tf.random.set_seed(42) # initialize neural network weights and biases to same values everytime to ensure reproducibility
model = models.Sequential() # create feed forward back propagating neural network
model.add(layers.Dense(5, activation='sigmoid', input_shape=(1,))) # 1st hidden layer, input_shape=(1,) means input must be column vector
model.add(layers.Dense(1)) # output layer, no activation in case of regression problems
custom_optimizer = optimizers.Adam(learning_rate=0.1) # learning rate for Adam optimizer
model.compile(optimizer=custom_optimizer, loss='mean_squared_error') # choose loss function
Results are compared between TensorFlow and my own code [here] Fig. 2. For a simple case shown, TensorFlow takes 2.5s from pressing the run button to plotting. My own code takes 0.5 s. It's neck and neck đ¤Ŗ. Obviously, learning rate, hidden layers, hidden layer neurons, epochs and optimizers are same in both cases.
Fig. 2, Own code, VS analytical VS TF
If you want to hire me as your awesome PhD student, please reach out! Thank you very much for reading!