From a38e15978944f53651659bf793653cac844fa284 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Sun, 14 Sep 2025 09:11:37 -0700 Subject: [PATCH 001/142] First pass at minimal wrapping model --- .../HeatEquation/Source/HeatEquationModel.py | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100755 GuidedTutorials/HeatEquation/Source/HeatEquationModel.py diff --git a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py new file mode 100755 index 00000000..bcd2c97a --- /dev/null +++ b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import amrex.space3d as amr +import numpy as np + + +def load_cupy(): + """Load GPU backend if available.""" + if amr.Config.have_gpu: + try: + import cupy as cp + xp = cp + amr.Print("Note: found and will use cupy") + except ImportError: + amr.Print("Warning: GPU found but cupy not available! Trying managed memory in numpy...") + import numpy as np + xp = np + if amr.Config.gpu_backend == "SYCL": + amr.Print("Warning: SYCL GPU backend not yet implemented for Python") + import numpy as np + xp = np + + else: + import numpy as np + xp = np + amr.Print("Note: found and will use numpy") + return xp + + +def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, + n_cell=32, max_grid_size=16, nsteps=100, plot_int=100, dt=1e-5): + """ + Run heat equation with given parameters and return final state metrics. + + Returns: [max_value, mean_value, std_dev, total_heat, center_value] + """ + plot_files_output = False + # CPU/GPU logic + xp = load_cupy() + + # AMREX_D_DECL means "do the first X of these, where X is the dimensionality of the simulation" + dom_lo = amr.IntVect(*amr.d_decl( 0, 0, 0)) + dom_hi = amr.IntVect(*amr.d_decl(n_cell-1, n_cell-1, n_cell-1)) + + # Make a single box that is the entire domain + domain = amr.Box(dom_lo, dom_hi) + + # Make BoxArray and Geometry: + # ba contains a list of boxes that cover the domain, + # geom contains information such as the physical domain size, + # number of points in the domain, and periodicity + + # Initialize the boxarray "ba" from the single box "domain" + ba = amr.BoxArray(domain) + # Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction + ba.max_size(max_grid_size) + + # This defines the physical box, [0,1] in each direction. + real_box = amr.RealBox([*amr.d_decl( 0., 0., 0.)], [*amr.d_decl( 1., 1., 1.)]) + + # This defines a Geometry object + # periodic in all direction + coord = 0 # Cartesian + is_per = [*amr.d_decl(1,1,1)] # periodicity + geom = amr.Geometry(domain, real_box, coord, is_per); + + # Extract dx from the geometry object + dx = geom.data().CellSize() + + # Nghost = number of ghost cells for each array + Nghost = 1 + + # Ncomp = number of components for each array + Ncomp = 1 + + # How Boxes are distrubuted among MPI processes + dm = amr.DistributionMapping(ba) + + # Allocate two phi multifabs: one will store the old state, the other the new. + phi_old = amr.MultiFab(ba, dm, Ncomp, Nghost) + phi_new = amr.MultiFab(ba, dm, Ncomp, Nghost) + phi_old.set_val(0.) + phi_new.set_val(0.) + + # time = starting time in the simulation + time = 0. + + # Ghost cells + ng = phi_old.n_grow_vect + ngx = ng[0] + ngy = ng[1] + ngz = ng[2] + + # Initialize with parameterized initial condition + for mfi in phi_old: + bx = mfi.validbox() + # phiOld is indexed in reversed order (z,y,x) and indices are local + phiOld = xp.array(phi_old.array(mfi), copy=False) + + x = (xp.arange(bx.small_end[0], bx.big_end[0]+1, 1) + 0.5) * dx[0] + y = (xp.arange(bx.small_end[1], bx.big_end[1]+1, 1) + 0.5) * dx[1] + z = (xp.arange(bx.small_end[2], bx.big_end[2]+1, 1) + 0.5) * dx[2] + + rsquared = ((z[:, xp.newaxis, xp.newaxis] - 0.5)**2 + + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 + + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / init_width + phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + init_amplitude * xp.exp(-rsquared) + + # Write a plotfile of the initial data if plot_int > 0 + if plot_int > 0 and plot_files_output: + step = 0 + pltfile = amr.concatenate("plt", step, 5) + varnames = amr.Vector_string(['phi']) + amr.write_single_level_plotfile(pltfile, phi_old, varnames, geom, time, 0) + + # Time evolution + for step in range(1, nsteps+1): + phi_old.fill_boundary(geom.periodicity()) + + # new_phi = old_phi + dt * Laplacian(old_phi) + # Loop over boxes + for mfi in phi_old: + phiOld = xp.array(phi_old.array(mfi), copy=False) + phiNew = xp.array(phi_new.array(mfi), copy=False) + hix = phiOld.shape[3] + hiy = phiOld.shape[2] + hiz = phiOld.shape[1] + + # Heat equation with parameterized diffusion + # Advance the data by dt + phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( + phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + dt * diffusion_coeff * + (( phiOld[:, ngz :-ngz , ngy :-ngy , ngx+1:hix-ngx+1] + -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] + +phiOld[:, ngz :-ngz , ngy :-ngy , ngx-1:hix-ngx-1]) / dx[0]**2 + +( phiOld[:, ngz :-ngz , ngy+1:hiy-ngy+1, ngx :-ngx ] + -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] + +phiOld[:, ngz :-ngz , ngy-1:hiy-ngy-1, ngx :-ngx ]) / dx[1]**2 + +( phiOld[:, ngz+1:hiz-ngz+1, ngy :-ngy , ngx :-ngx ] + -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] + +phiOld[:, ngz-1:hiz-ngz-1, ngy :-ngy , ngx :-ngx ]) / dx[2]**2)) + + # Update time + time = time + dt + + # Copy new solution into old solution + amr.copy_mfab(dst=phi_old, src=phi_new, srccomp=0, dstcomp=0, numcomp=1, nghost=0) + # Tell the I/O Processor to write out which step we're doing + # amr.Print(f'Advanced step {step}\n') + + # Write a plotfile of the current data (plot_int was defined in the inputs file) + if plot_int > 0 and step%plot_int == 0 and plot_files_output: + pltfile = amr.concatenate("plt", step, 5) + varnames = amr.Vector_string(['phi']) + amr.write_single_level_plotfile(pltfile, phi_new, varnames, geom, time, step) + + # Compute output metrics from final state + all_data = [] + for mfi in phi_new: + phi_arr = xp.array(phi_new.array(mfi), copy=False) + valid_data = phi_arr[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] + if xp.__name__ == 'cupy': + valid_data = valid_data.get() + all_data.append(valid_data.flatten()) + + all_data = np.concatenate(all_data) + + return np.array([ + np.max(all_data), # max value + np.mean(all_data), # mean value + np.std(all_data), # std dev + np.sum(all_data) * dx[0] * dx[1] * dx[2], # integral + all_data[len(all_data)//2] # center value (approximate) + ]) + + +class HeatEquationModel: + """Simple wrapper to make heat equation callable with parameter arrays.""" + + def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, dt=1e-5): + self.n_cell = n_cell + self.max_grid_size = max_grid_size + self.nsteps = nsteps + self.plot_int = 100 + self.dt = dt + + def __call__(self, params): + """ + Run heat equation for each parameter set. + + Parameters: + ----------- + params : numpy.ndarray of shape (n_samples, 3) + params[:, 0] = diffusion coefficient + params[:, 1] = initial condition amplitude + params[:, 2] = initial condition width + + Returns: + -------- + numpy.ndarray of shape (n_samples, 5) + [max, mean, std, integral, center] for each sample + """ + if params.ndim == 1: + params = params.reshape(1, -1) + + n_samples = params.shape[0] + outputs = np.zeros((n_samples, 5)) + + for i in range(n_samples): + outputs[i, :] = heat_equation_run( + diffusion_coeff=params[i, 0], + init_amplitude=params[i, 1], + init_width=params[i, 2], + n_cell=self.n_cell, + max_grid_size=self.max_grid_size, + nsteps=self.nsteps, + plot_int=self.plot_int, + dt=self.dt + ) + + return outputs + +if __name__ == "__main__": + + # Initialize AMReX + amr.initialize([]) + + # Example usage + model = HeatEquationModel(n_cell=32, nsteps=100) + + # Test with random parameters + test_params = np.array([ + [1.0, 1.0, 0.01], # baseline + [2.0, 1.5, 0.02], # higher diffusion, higher amplitude + [0.5, 2.0, 0.005] # lower diffusion, higher amplitude, narrower + ]) + + print("Running heat equation with parameters:") + print(" [diffusion, amplitude, width]") + print(test_params) + + outputs = model(test_params) + + print("\nResults [max, mean, std, integral, center]:") + print(outputs) + + # Finalize AMReX + amr.finalize() + From d57dd2104076af5a68100a3ec03d7fca7641bc03 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Sun, 14 Sep 2025 10:25:32 -0700 Subject: [PATCH 002/142] Test with just the black box function --- .../HeatEquation/Source/HeatEquationModel.py | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py index bcd2c97a..e8e66b7c 100755 --- a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py @@ -178,7 +178,7 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, class HeatEquationModel: """Simple wrapper to make heat equation callable with parameter arrays.""" - def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, dt=1e-5): + def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, plot_int=100, dt=1e-5): self.n_cell = n_cell self.max_grid_size = max_grid_size self.nsteps = nsteps @@ -221,6 +221,51 @@ def __call__(self, params): return outputs +class IshigamiSimple: + """Simple wrapper to make heat equation callable with parameter arrays.""" + + def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, plot_int=100, dt=1e-5): + self.n_cell = n_cell + self.max_grid_size = max_grid_size + self.nsteps = nsteps + self.plot_int = 100 + self.dt = dt + + def __call__(self, params): + """ + Run heat equation for each parameter set. + + Parameters: + ----------- + params : numpy.ndarray of shape (n_samples, 3) + params[:, 0] = diffusion coefficient + params[:, 1] = initial condition amplitude + params[:, 2] = initial condition width + + Returns: + -------- + numpy.ndarray of shape (n_samples, 5) + [max, mean, std, integral, center] for each sample + """ + if params.ndim == 1: + params = params.reshape(1, -1) + + n_samples = params.shape[0] + outputs = np.zeros((n_samples, 5)) + + for i in range(n_samples): + x1 = params[i, 0] + x2 = params[i, 1] + x3 = params[i, 2] + + outputs[i, 0] = np.exp(x1) + outputs[i, 1] = x3 * np.log(x1**2) + x1 + (x2**2) * (1 - np.exp(-x3**2)) + outputs[i, 2] = x1 + x3**2 + outputs[i, 3] = x2 + x3 + outputs[i, 4] = x1 * x3 + + return outputs + if __name__ == "__main__": # Initialize AMReX From e8ff270e33c4d6459e61040f20fa6a107fe1773f Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 15 Sep 2025 07:08:27 -0700 Subject: [PATCH 003/142] Fix spacing --- .../HeatEquation/Source/HeatEquationModel.py | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py index e8e66b7c..24fc008e 100755 --- a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py @@ -32,7 +32,7 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, n_cell=32, max_grid_size=16, nsteps=100, plot_int=100, dt=1e-5): """ Run heat equation with given parameters and return final state metrics. - + Returns: [max_value, mean_value, std_dev, total_heat, center_value] """ plot_files_output = False @@ -97,11 +97,11 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, bx = mfi.validbox() # phiOld is indexed in reversed order (z,y,x) and indices are local phiOld = xp.array(phi_old.array(mfi), copy=False) - + x = (xp.arange(bx.small_end[0], bx.big_end[0]+1, 1) + 0.5) * dx[0] y = (xp.arange(bx.small_end[1], bx.big_end[1]+1, 1) + 0.5) * dx[1] z = (xp.arange(bx.small_end[2], bx.big_end[2]+1, 1) + 0.5) * dx[2] - + rsquared = ((z[:, xp.newaxis, xp.newaxis] - 0.5)**2 + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / init_width @@ -126,11 +126,11 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, hix = phiOld.shape[3] hiy = phiOld.shape[2] hiz = phiOld.shape[1] - + # Heat equation with parameterized diffusion # Advance the data by dt phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( - phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + dt * diffusion_coeff * + phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + dt * diffusion_coeff * (( phiOld[:, ngz :-ngz , ngy :-ngy , ngx+1:hix-ngx+1] -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] +phiOld[:, ngz :-ngz , ngy :-ngy , ngx-1:hix-ngx-1]) / dx[0]**2 @@ -163,12 +163,12 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, if xp.__name__ == 'cupy': valid_data = valid_data.get() all_data.append(valid_data.flatten()) - + all_data = np.concatenate(all_data) - + return np.array([ np.max(all_data), # max value - np.mean(all_data), # mean value + np.mean(all_data), # mean value np.std(all_data), # std dev np.sum(all_data) * dx[0] * dx[1] * dx[2], # integral all_data[len(all_data)//2] # center value (approximate) @@ -177,25 +177,25 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, class HeatEquationModel: """Simple wrapper to make heat equation callable with parameter arrays.""" - + def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, plot_int=100, dt=1e-5): self.n_cell = n_cell self.max_grid_size = max_grid_size self.nsteps = nsteps self.plot_int = 100 self.dt = dt - + def __call__(self, params): """ Run heat equation for each parameter set. - + Parameters: ----------- params : numpy.ndarray of shape (n_samples, 3) params[:, 0] = diffusion coefficient - params[:, 1] = initial condition amplitude + params[:, 1] = initial condition amplitude params[:, 2] = initial condition width - + Returns: -------- numpy.ndarray of shape (n_samples, 5) @@ -203,10 +203,10 @@ def __call__(self, params): """ if params.ndim == 1: params = params.reshape(1, -1) - + n_samples = params.shape[0] outputs = np.zeros((n_samples, 5)) - + for i in range(n_samples): outputs[i, :] = heat_equation_run( diffusion_coeff=params[i, 0], @@ -218,30 +218,30 @@ def __call__(self, params): plot_int=self.plot_int, dt=self.dt ) - + return outputs class IshigamiSimple: """Simple wrapper to make heat equation callable with parameter arrays.""" - + def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, plot_int=100, dt=1e-5): self.n_cell = n_cell self.max_grid_size = max_grid_size self.nsteps = nsteps self.plot_int = 100 self.dt = dt - + def __call__(self, params): """ Run heat equation for each parameter set. - + Parameters: ----------- params : numpy.ndarray of shape (n_samples, 3) params[:, 0] = diffusion coefficient - params[:, 1] = initial condition amplitude + params[:, 1] = initial condition amplitude params[:, 2] = initial condition width - + Returns: -------- numpy.ndarray of shape (n_samples, 5) @@ -249,10 +249,10 @@ def __call__(self, params): """ if params.ndim == 1: params = params.reshape(1, -1) - + n_samples = params.shape[0] outputs = np.zeros((n_samples, 5)) - + for i in range(n_samples): x1 = params[i, 0] x2 = params[i, 1] @@ -263,7 +263,7 @@ def __call__(self, params): outputs[i, 2] = x1 + x3**2 outputs[i, 3] = x2 + x3 outputs[i, 4] = x1 * x3 - + return outputs if __name__ == "__main__": @@ -273,20 +273,20 @@ def __call__(self, params): # Example usage model = HeatEquationModel(n_cell=32, nsteps=100) - + # Test with random parameters test_params = np.array([ [1.0, 1.0, 0.01], # baseline [2.0, 1.5, 0.02], # higher diffusion, higher amplitude [0.5, 2.0, 0.005] # lower diffusion, higher amplitude, narrower ]) - + print("Running heat equation with parameters:") print(" [diffusion, amplitude, width]") print(test_params) - + outputs = model(test_params) - + print("\nResults [max, mean, std, integral, center]:") print(outputs) From ac3da5012b8753000b22db5d6f370e0614a4fa79 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 15 Sep 2025 07:16:53 -0700 Subject: [PATCH 004/142] Clean up naming and comments --- .../HeatEquation/Source/HeatEquationModel.py | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py index 24fc008e..1a4bac30 100755 --- a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py @@ -221,31 +221,23 @@ def __call__(self, params): return outputs -class IshigamiSimple: - """Simple wrapper to make heat equation callable with parameter arrays.""" - - def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, plot_int=100, dt=1e-5): - self.n_cell = n_cell - self.max_grid_size = max_grid_size - self.nsteps = nsteps - self.plot_int = 100 - self.dt = dt +class IshigamiSurrogate: + """Black box model that might be a surrogate for Ishigami.""" def __call__(self, params): """ - Run heat equation for each parameter set. + Run equations for each parameter set. Parameters: ----------- params : numpy.ndarray of shape (n_samples, 3) - params[:, 0] = diffusion coefficient - params[:, 1] = initial condition amplitude - params[:, 2] = initial condition width + params[:, 0] = x1 + params[:, 1] = x2 + params[:, 2] = x3 Returns: -------- - numpy.ndarray of shape (n_samples, 5) - [max, mean, std, integral, center] for each sample + numpy.ndarray of shape (n_samples, 5) for each sample """ if params.ndim == 1: params = params.reshape(1, -1) From 8897f419d5559d30edaff4a6597876baad0581fe Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 15 Sep 2025 11:23:32 -0700 Subject: [PATCH 005/142] Make sure amr initialized --- GuidedTutorials/HeatEquation/Source/HeatEquationModel.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py index 1a4bac30..153d3306 100755 --- a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py @@ -185,6 +185,10 @@ def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, plot_int=100, dt=1e self.plot_int = 100 self.dt = dt + # Conditionally initialize AMReX + if not amr.initialized(): + amr.initialize([]) + def __call__(self, params): """ Run heat equation for each parameter set. From 3627022578054ca7d94d6965901c48ef00a19d0b Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 16 Sep 2025 06:10:22 -0700 Subject: [PATCH 006/142] Make return vals more readable --- .../HeatEquation/Source/HeatEquationModel.py | 53 ++++++++++++++++--- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py index 153d3306..39dd7ffc 100755 --- a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py @@ -156,16 +156,55 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, amr.write_single_level_plotfile(pltfile, phi_new, varnames, geom, time, step) # Compute output metrics from final state - all_data = [] + + # Find center value at (0.5, 0.5, 0.5) + center_x, center_y, center_z = 0.5, 0.5, 0.5 + + # Convert physical coordinates to global cell indices + i_center = int(center_x / dx[0] - 0.5) + j_center = int(center_y / dx[1] - 0.5) + k_center = int(center_z / dx[2] - 0.5) + + center_val = None for mfi in phi_new: - phi_arr = xp.array(phi_new.array(mfi), copy=False) - valid_data = phi_arr[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] - if xp.__name__ == 'cupy': - valid_data = valid_data.get() - all_data.append(valid_data.flatten()) + bx = mfi.validbox() + + # Check if this box contains the center point + if bx.contains([i_center, j_center, k_center]): + phi_arr = xp.array(phi_new.array(mfi), copy=False) - all_data = np.concatenate(all_data) + # Convert global indices to local array indices + local_i = i_center - bx.small_end[0] + ngx + local_j = j_center - bx.small_end[1] + ngy + local_k = k_center - bx.small_end[2] + ngz + # Extract center value (array indexed as [z,y,x]) + center_val = float(phi_arr[0, local_k, local_j, local_i]) + if xp.__name__ == 'cupy': + center_val = float(center_val) + break + + if center_val is None: + center_val = 0.0 + + # Compute output metrics from final state using PyAMReX built-ins + max_val = phi_new.max(nghost=0) # exclude ghost zones + mean_val = phi_new.sum(nghost=0) / phi_new.box_array().numPts() + sum_val = phi_new.sum(nghost=0) + integral = sum_val * dx[0] * dx[1] * dx[2] + + # For standard deviation, you'll need to compute it in two steps + sum_sq = phi_new.dot(phi_new, nghost=0) # sum of squares + variance = (sum_sq / phi_new.box_array().numPts()) - mean_val**2 + std_val = np.sqrt(max(0, variance)) # max(0, ...) handles potential numerical issues + + return np.array([ + max_val, + mean_val, + std_val, + integral, + center_val +]) return np.array([ np.max(all_data), # max value np.mean(all_data), # mean value From a1335870ed1790db59cfce1006193293fc5abee1 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 16 Sep 2025 06:22:27 -0700 Subject: [PATCH 007/142] Tweak output calculation --- .../HeatEquation/Source/HeatEquationModel.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py index 39dd7ffc..087d015b 100755 --- a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py @@ -169,8 +169,11 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, for mfi in phi_new: bx = mfi.validbox() + # Create IntVect3D for the center point + center_iv = amr.IntVect3D(i_center, j_center, k_center) + # Check if this box contains the center point - if bx.contains([i_center, j_center, k_center]): + if bx.contains(center_iv): phi_arr = xp.array(phi_new.array(mfi), copy=False) # Convert global indices to local array indices @@ -188,15 +191,20 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, center_val = 0.0 # Compute output metrics from final state using PyAMReX built-ins - max_val = phi_new.max(nghost=0) # exclude ghost zones - mean_val = phi_new.sum(nghost=0) / phi_new.box_array().numPts() - sum_val = phi_new.sum(nghost=0) - integral = sum_val * dx[0] * dx[1] * dx[2] + max_val = phi_new.max(comp=0, local=False) + sum_val = phi_new.sum(comp=0, local=False) - # For standard deviation, you'll need to compute it in two steps - sum_sq = phi_new.dot(phi_new, nghost=0) # sum of squares - variance = (sum_sq / phi_new.box_array().numPts()) - mean_val**2 - std_val = np.sqrt(max(0, variance)) # max(0, ...) handles potential numerical issues + # Get total number of valid cells (excluding ghost zones) + total_cells = phi_new.box_array().numPts + mean_val = sum_val / total_cells + + # Use L2 norm for standard deviation calculation + l2_norm = phi_new.norm2(0) + sum_sq = l2_norm**2 + variance = (sum_sq / total_cells) - mean_val**2 + std_val = np.sqrt(max(0, variance)) + + integral = sum_val * dx[0] * dx[1] * dx[2] return np.array([ max_val, @@ -204,16 +212,8 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, std_val, integral, center_val -]) - return np.array([ - np.max(all_data), # max value - np.mean(all_data), # mean value - np.std(all_data), # std dev - np.sum(all_data) * dx[0] * dx[1] * dx[2], # integral - all_data[len(all_data)//2] # center value (approximate) ]) - class HeatEquationModel: """Simple wrapper to make heat equation callable with parameter arrays.""" From 816476922b90e9f2461914f9e5b8a5fcb928e0ce Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Wed, 24 Sep 2025 06:20:12 -0700 Subject: [PATCH 008/142] Use inputs file and parmparse --- .../HeatEquation/Source/HeatEquationModel.py | 116 ++++++++++++++++-- 1 file changed, 103 insertions(+), 13 deletions(-) diff --git a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py index 087d015b..a8756dda 100755 --- a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py @@ -214,19 +214,80 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, center_val ]) + +def parse_inputs(): + """Parse inputs using AMReX ParmParse interface.""" + pp = amr.ParmParse("") + + # Add inputs file if it exists + import os + inputs_file = "inputs" + if os.path.exists(inputs_file): + pp.addfile(inputs_file) + + # Read simulation parameters with defaults + n_cell = 32 + pp.query("n_cell", n_cell) + + max_grid_size = 16 + pp.query("max_grid_size", max_grid_size) + + nsteps = 1000 + pp.query("nsteps", nsteps) + + plot_int = 100 + pp.query("plot_int", plot_int) + + dt = 1.0e-5 + pp.query("dt", dt) + + # Read heat equation model parameters with defaults + diffusion_coeff = 1.0 + pp.query("diffusion_coeff", diffusion_coeff) + + init_amplitude = 1.0 + pp.query("init_amplitude", init_amplitude) + + init_width = 0.01 + pp.query("init_width", init_width) + + return { + 'n_cell': n_cell, + 'max_grid_size': max_grid_size, + 'nsteps': nsteps, + 'plot_int': plot_int, + 'dt': dt, + 'diffusion_coeff': diffusion_coeff, + 'init_amplitude': init_amplitude, + 'init_width': init_width + } + class HeatEquationModel: """Simple wrapper to make heat equation callable with parameter arrays.""" - def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, plot_int=100, dt=1e-5): - self.n_cell = n_cell - self.max_grid_size = max_grid_size - self.nsteps = nsteps - self.plot_int = 100 - self.dt = dt - - # Conditionally initialize AMReX - if not amr.initialized(): - amr.initialize([]) + def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, plot_int=100, dt=1e-5, use_parmparse=False): + if use_parmparse: + # Conditionally initialize AMReX first if using ParmParse + if not amr.initialized(): + amr.initialize([]) + + # Parse inputs from file + params = parse_inputs() + self.n_cell = params['n_cell'] + self.max_grid_size = params['max_grid_size'] + self.nsteps = params['nsteps'] + self.plot_int = params['plot_int'] + self.dt = params['dt'] + else: + self.n_cell = n_cell + self.max_grid_size = max_grid_size + self.nsteps = nsteps + self.plot_int = plot_int + self.dt = dt + + # Conditionally initialize AMReX + if not amr.initialized(): + amr.initialize([]) def __call__(self, params): """ @@ -238,11 +299,13 @@ def __call__(self, params): params[:, 0] = diffusion coefficient params[:, 1] = initial condition amplitude params[:, 2] = initial condition width + (Use get_pnames() to get these names programmatically) Returns: -------- numpy.ndarray of shape (n_samples, 5) [max, mean, std, integral, center] for each sample + (Use get_outnames() to get these names programmatically) """ if params.ndim == 1: params = params.reshape(1, -1) @@ -264,6 +327,26 @@ def __call__(self, params): return outputs + def get_pnames(self): + """ + Get parameter names for the heat equation model. + + Returns: + -------- + list : Parameter names corresponding to the input dimensions + """ + return ["diffusion coefficient", "initial condition amplitude", "initial condition width"] + + def get_outnames(self): + """ + Get output names for the heat equation model. + + Returns: + -------- + list : Output names corresponding to the computed quantities + """ + return ["max", "mean", "std", "integral", "center"] + class IshigamiSurrogate: """Black box model that might be a surrogate for Ishigami.""" @@ -306,8 +389,15 @@ def __call__(self, params): # Initialize AMReX amr.initialize([]) - # Example usage - model = HeatEquationModel(n_cell=32, nsteps=100) + # Create model using ParmParse to read from inputs file + model = HeatEquationModel(use_parmparse=True) + + print(f"Heat equation model initialized with:") + print(f" n_cell = {model.n_cell}") + print(f" max_grid_size = {model.max_grid_size}") + print(f" nsteps = {model.nsteps}") + print(f" plot_int = {model.plot_int}") + print(f" dt = {model.dt}") # Test with random parameters test_params = np.array([ @@ -316,7 +406,7 @@ def __call__(self, params): [0.5, 2.0, 0.005] # lower diffusion, higher amplitude, narrower ]) - print("Running heat equation with parameters:") + print("\nRunning heat equation with parameters:") print(" [diffusion, amplitude, width]") print(test_params) From ef2a52a5f0e2035c2235a5bda3537b610c67d5ba Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Wed, 24 Sep 2025 17:22:06 -0700 Subject: [PATCH 009/142] Add driver from python with pybind11 --- .../HeatEquation_PythonDriver/CMakeLists.txt | 77 +++++++++ .../HeatEquation_PythonDriver/README.md | 89 ++++++++++ .../HeatEquation_PythonDriver/bindings.cpp | 49 ++++++ .../HeatEquation_PythonDriver/inputs | 7 + .../HeatEquation_PythonDriver/main.cpp | 163 ++++++++++++++++++ .../HeatEquation_PythonDriver/pybind11.cmake | 63 +++++++ .../HeatEquation_PythonDriver/test.py | 37 ++++ 7 files changed, 485 insertions(+) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/CMakeLists.txt create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/README.md create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/bindings.cpp create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/inputs create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/main.cpp create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/pybind11.cmake create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/test.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/CMakeLists.txt b/GuidedTutorials/HeatEquation_PythonDriver/CMakeLists.txt new file mode 100644 index 00000000..1c005a0a --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/CMakeLists.txt @@ -0,0 +1,77 @@ +# CMake file for Heat Equation Python Driver Example +# +# This demonstrates the minimal setup needed for a Python interface +# to AMReX C++ simulation code. +# +# Step 1: Create and enter a build directory: +# +# mkdir build +# cd build +# +# Step 2: Configure with CMake from inside the build directory: +# +# cmake .. +# +# Step 3: Build the configuration: +# +# cmake --build . -j4 +# +# Step 4: Test the Python interface: +# +# python ../test.py + +cmake_minimum_required(VERSION 3.16) + +# Project name and source file languages +project(HeatEquation_PythonDriver + LANGUAGES C CXX) + +# Use pyamrex pybind11 infrastructure +include(pybind11.cmake) + +# Add the main executable +add_executable(HeatEquation_PythonDriver main.cpp) + +# Add the pybind11 module including main simulation logic +pybind11_add_module(amrex_heat bindings.cpp main.cpp) + +# To use a pre-installed AMReX build, run: +# cmake -DAMReX_ROOT=/path/to/installdir +# Otherwise cmake will download AMReX from GitHub + +if(NOT DEFINED AMReX_ROOT) + message("-- Download and configure AMReX from GitHub") + + #Download AMReX from GitHub + include(FetchContent) + set(FETCHCONTENT_QUIET OFF) + + # Force position-independent code for shared libraries + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + + FetchContent_Declare( + amrex_code + GIT_REPOSITORY https://github.com/AMReX-Codes/amrex.git/ + GIT_TAG origin/development + ) + + FetchContent_Populate(amrex_code) + + # CMake will read the files in these directories to configure, build + # and install AMReX. + add_subdirectory(${amrex_code_SOURCE_DIR} ${amrex_code_BINARY_DIR}) + +else() + + # Add AMReX install + message("-- Searching for AMReX install directory at ${AMReX_ROOT}") + find_package(AMReX PATHS ${AMReX_ROOT}/lib/cmake/AMReX/AMReXConfig.cmake) + +endif() + +# Link AMReX to both targets +target_link_libraries(HeatEquation_PythonDriver PRIVATE AMReX::amrex) +target_link_libraries(amrex_heat PRIVATE AMReX::amrex) + +## Copy input files to build directory +file( COPY ${CMAKE_SOURCE_DIR}/inputs DESTINATION . ) \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_PythonDriver/README.md b/GuidedTutorials/HeatEquation_PythonDriver/README.md new file mode 100644 index 00000000..ed0a9f8a --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/README.md @@ -0,0 +1,89 @@ +# Heat Equation Python Driver + +This example demonstrates the minimal setup needed to create a one-line Python interface for AMReX C++ simulation codes. + +## Key Feature + +The main goal is to enable this simple Python usage pattern: + +```python +import amrex_heat + +# One-line simulation execution with structured results +result = amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"]) + +print(f"Max temperature: {result.max_temperature}") +print(f"Final time: {result.final_time}") +``` + +## Files + +- `main.cpp` - Heat equation solver with `heat_equation_main()` function +- `bindings.cpp` - Minimal pybind11 interface (just the one-liner) +- `CMakeLists.txt` - Build configuration with pybind11 support +- `pybind11.cmake` - Pybind11 infrastructure from pyamrex +- `inputs` - Simulation parameters +- `test.py` - Example Python usage + +## Architecture + +The key architectural insight is the function refactoring pattern: + +```cpp +// Reusable simulation function +SimulationResult heat_equation_main(int argc, char* argv[]) { + // All simulation logic here + return result; +} + +// C++ entry point +int main(int argc, char* argv[]) { + heat_equation_main(argc, argv); + return 0; +} +``` + +This allows the same simulation code to be called from: +- Command line: `./HeatEquation_PythonDriver inputs` +- Python: `amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"])` + +## Building + +```bash +mkdir build && cd build +cmake .. +make -j4 +``` + +## Running + +### C++ executable +```bash +cd build +./HeatEquation_PythonDriver inputs +``` + +### Python interface +```bash +cd build +python ../test.py +``` + +## What's Minimal + +This example includes only the essential components: + +1. **SimulationResult struct** - Simple data structure for return values +2. **heat_equation_main function** - Refactored simulation code +3. **Minimal bindings** - Just `run_simulation()` function +4. **Basic CMake** - Pybind11 integration + +No callback system, no complex data structures, no advanced features - just the core one-liner interface. + +## Extending + +This foundation can be extended with: +- Callback system for progress monitoring +- More return data (arrays, MultiFabs) +- Parameter setting from Python +- Integration with full pyamrex infrastructure \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_PythonDriver/bindings.cpp b/GuidedTutorials/HeatEquation_PythonDriver/bindings.cpp new file mode 100644 index 00000000..c4d1446f --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/bindings.cpp @@ -0,0 +1,49 @@ +/* + * Minimal Python bindings for Heat Equation + * + * This provides just the essential one-line interface: + * result = amrex_heat.run_simulation(["./executable", "inputs"]) + */ + +#include +#include + +// Forward declaration from main.cpp +struct SimulationResult { + double max_temperature; + int final_step; + double final_time; + bool success; +}; + +SimulationResult heat_equation_main(int argc, char* argv[]); + +namespace py = pybind11; + +PYBIND11_MODULE(amrex_heat, m) { + m.doc() = "Minimal AMReX Heat Equation Python Interface"; + + // Expose SimulationResult struct + py::class_(m, "SimulationResult") + .def_readonly("max_temperature", &SimulationResult::max_temperature) + .def_readonly("final_step", &SimulationResult::final_step) + .def_readonly("final_time", &SimulationResult::final_time) + .def_readonly("success", &SimulationResult::success); + + // Main simulation function - one-liner interface + m.def("run_simulation", [](py::list args) { + // Convert Python list to C++ argc/argv with proper lifetime management + std::vector args_str; + for (auto item : args) { + args_str.push_back(py::str(item)); + } + + std::vector args_cstr; + for (auto& s : args_str) { + args_cstr.push_back(const_cast(s.c_str())); + } + args_cstr.push_back(nullptr); // Null terminate + + return heat_equation_main(static_cast(args_cstr.size() - 1), args_cstr.data()); + }, "Run the heat equation simulation and return results", py::arg("args")); +} \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_PythonDriver/inputs b/GuidedTutorials/HeatEquation_PythonDriver/inputs new file mode 100644 index 00000000..5b098f82 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/inputs @@ -0,0 +1,7 @@ +n_cell = 32 +max_grid_size = 16 + +nsteps = 1000 +plot_int = 100 + +dt = 1.e-5 diff --git a/GuidedTutorials/HeatEquation_PythonDriver/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/main.cpp new file mode 100644 index 00000000..e6fa75e9 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/main.cpp @@ -0,0 +1,163 @@ +/* + * Simplified Heat Equation with Python Driver Interface + * + * This demonstrates the minimal setup needed for a one-line Python interface + * that calls C++ simulation code and returns structured results. + */ + +#include +#include +#include + +// Simple result structure for Python interface +struct SimulationResult { + double max_temperature; + int final_step; + double final_time; + bool success; +}; + +SimulationResult heat_equation_main(int argc, char* argv[]) +{ + SimulationResult result = {0.0, 0, 0.0, false}; + + amrex::Initialize(argc,argv); + { + + // ********************************** + // DECLARE SIMULATION PARAMETERS + // ********************************** + + int n_cell; + int max_grid_size; + int nsteps; + int plot_int; + amrex::Real dt; + + // ********************************** + // READ PARAMETER VALUES FROM INPUT DATA + // ********************************** + { + amrex::ParmParse pp; + pp.get("n_cell",n_cell); + pp.get("max_grid_size",max_grid_size); + nsteps = 10; + pp.query("nsteps",nsteps); + plot_int = -1; + pp.query("plot_int",plot_int); + pp.get("dt",dt); + } + + // ********************************** + // DEFINE SIMULATION SETUP AND GEOMETRY + // ********************************** + + amrex::BoxArray ba; + amrex::Geometry geom; + + amrex::IntVect dom_lo(0,0,0); + amrex::IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); + + amrex::Box domain(dom_lo, dom_hi); + ba.define(domain); + ba.maxSize(max_grid_size); + + amrex::RealBox real_box({ 0., 0., 0.}, { 1., 1., 1.}); + amrex::Array is_periodic{1,1,1}; + geom.define(domain, real_box, amrex::CoordSys::cartesian, is_periodic); + + amrex::GpuArray dx = geom.CellSizeArray(); + + int Nghost = 1; + int Ncomp = 1; + + amrex::DistributionMapping dm(ba); + amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); + amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); + + amrex::Real time = 0.0; + + // ********************************** + // INITIALIZE DATA LOOP + // ********************************** + + for (amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi) + { + const amrex::Box& bx = mfi.validbox(); + const amrex::Array4& phiOld = phi_old.array(mfi); + + amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) + { + amrex::Real x = (i+0.5) * dx[0]; + amrex::Real y = (j+0.5) * dx[1]; + amrex::Real z = (k+0.5) * dx[2]; + amrex::Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/0.01; + phiOld(i,j,k) = 1. + std::exp(-rsquared); + }); + } + + // ********************************** + // WRITE INITIAL PLOT FILE + // ********************************** + + if (plot_int > 0) + { + int step = 0; + const std::string& pltfile = amrex::Concatenate("plt",step,5); + WriteSingleLevelPlotfile(pltfile, phi_old, {"phi"}, geom, time, 0); + } + + // ********************************** + // MAIN TIME EVOLUTION LOOP + // ********************************** + + for (int step = 1; step <= nsteps; ++step) + { + phi_old.FillBoundary(geom.periodicity()); + + for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ) + { + const amrex::Box& bx = mfi.validbox(); + const amrex::Array4& phiOld = phi_old.array(mfi); + const amrex::Array4& phiNew = phi_new.array(mfi); + + amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) + { + phiNew(i,j,k) = phiOld(i,j,k) + dt * + ( (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) + +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) + +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]) + ); + }); + } + + time = time + dt; + amrex::MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); + + // Update result with current state + result.final_step = step; + result.final_time = time; + + amrex::Print() << "Advanced step " << step << "\n"; + + if (plot_int > 0 && step%plot_int == 0) + { + const std::string& pltfile = amrex::Concatenate("plt",step,5); + WriteSingleLevelPlotfile(pltfile, phi_new, {"phi"}, geom, time, step); + } + } + + // Get final max temperature and mark success + result.max_temperature = phi_new.max(0); + result.success = true; + + } + amrex::Finalize(); + return result; +} + +int main(int argc, char* argv[]) +{ + heat_equation_main(argc, argv); + return 0; +} \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_PythonDriver/pybind11.cmake b/GuidedTutorials/HeatEquation_PythonDriver/pybind11.cmake new file mode 100644 index 00000000..45ba7898 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/pybind11.cmake @@ -0,0 +1,63 @@ +function(find_pybind11) + if(TARGET pybind11::module) + message(STATUS "pybind11::module target already imported") + elseif(pyAMReX_pybind11_src) + message(STATUS "Compiling local pybind11 ...") + message(STATUS "pybind11 source path: ${pyAMReX_pybind11_src}") + if(NOT IS_DIRECTORY ${pyAMReX_pybind11_src}) + message(FATAL_ERROR "Specified directory pyAMReX_pybind11_src='${pyAMReX_pybind11_src}' does not exist!") + endif() + elseif(pyAMReX_pybind11_internal) + message(STATUS "Downloading pybind11 ...") + message(STATUS "pybind11 repository: ${pyAMReX_pybind11_repo} (${pyAMReX_pybind11_branch})") + include(FetchContent) + endif() + + # rely on our find_package(Python ...) call + # https://pybind11.readthedocs.io/en/stable/compiling.html#modules-with-cmake + set(PYBIND11_FINDPYTHON ON) + + if(TARGET pybind11::module) + # nothing to do, target already exists in the superbuild + elseif(pyAMReX_pybind11_internal OR pyAMReX_pybind11_src) + set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) + + if(pyAMReX_pybind11_src) + add_subdirectory(${pyAMReX_pybind11_src} _deps/localpybind11-build/) + else() + FetchContent_Declare(fetchedpybind11 + GIT_REPOSITORY ${pyAMReX_pybind11_repo} + GIT_TAG ${pyAMReX_pybind11_branch} + BUILD_IN_SOURCE 0 + ) + FetchContent_MakeAvailable(fetchedpybind11) + + # advanced fetch options + mark_as_advanced(FETCHCONTENT_BASE_DIR) + mark_as_advanced(FETCHCONTENT_FULLY_DISCONNECTED) + mark_as_advanced(FETCHCONTENT_QUIET) + mark_as_advanced(FETCHCONTENT_SOURCE_DIR_FETCHEDpybind11) + mark_as_advanced(FETCHCONTENT_UPDATES_DISCONNECTED) + mark_as_advanced(FETCHCONTENT_UPDATES_DISCONNECTED_FETCHEDpybind11) + endif() + elseif(NOT pyAMReX_pybind11_internal) + find_package(pybind11 3.0.0 CONFIG REQUIRED) + message(STATUS "pybind11: Found version '${pybind11_VERSION}'") + endif() +endfunction() + +# local source-tree +set(pyAMReX_pybind11_src "" + CACHE PATH + "Local path to pybind11 source directory (preferred if set)") + +# Git fetcher +option(pyAMReX_pybind11_internal "Download & build pybind11" ON) +set(pyAMReX_pybind11_repo "https://github.com/pybind/pybind11.git" + CACHE STRING + "Repository URI to pull and build pybind11 from if(pyAMReX_pybind11_internal)") +set(pyAMReX_pybind11_branch "v3.0.0" + CACHE STRING + "Repository branch for pyAMReX_pybind11_repo if(pyAMReX_pybind11_internal)") + +find_pybind11() diff --git a/GuidedTutorials/HeatEquation_PythonDriver/test.py b/GuidedTutorials/HeatEquation_PythonDriver/test.py new file mode 100644 index 00000000..6aacaeef --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/test.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +""" +Simple test of the Heat Equation Python Driver + +This demonstrates the minimal one-line interface to run AMReX simulations +from Python and get structured results back. +""" + +import amrex_heat + +def main(): + print("Heat Equation Python Driver Test") + print("=" * 40) + + # The key feature: one-line simulation execution + print("Running simulation...") + result = amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"]) + + # Access results directly + print(f"\nSimulation Results:") + print(f" Success: {result.success}") + print(f" Final step: {result.final_step}") + print(f" Final time: {result.final_time:.6f}") + print(f" Max temperature: {result.max_temperature:.6f}") + + # You can use the results for further processing + if result.success: + print(f"\n✓ Simulation completed successfully!") + print(f" Temperature decay: {2.0 - result.max_temperature:.6f}") + else: + print(f"\n✗ Simulation failed!") + return 1 + + return 0 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file From 83c8ea12e6673655213aa739d026f8ed785ac803 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Wed, 24 Sep 2025 17:24:20 -0700 Subject: [PATCH 010/142] Add back comments --- .../HeatEquation_PythonDriver/main.cpp | 90 ++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/main.cpp index e6fa75e9..5c073e4f 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/main.cpp +++ b/GuidedTutorials/HeatEquation_PythonDriver/main.cpp @@ -5,6 +5,7 @@ * that calls C++ simulation code and returns structured results. */ + #include #include #include @@ -28,23 +29,48 @@ SimulationResult heat_equation_main(int argc, char* argv[]) // DECLARE SIMULATION PARAMETERS // ********************************** + // number of cells on each side of the domain int n_cell; + + // size of each box (or grid) int max_grid_size; + + // total steps in simulation int nsteps; + + // how often to write a plotfile int plot_int; + + // time step amrex::Real dt; // ********************************** // READ PARAMETER VALUES FROM INPUT DATA // ********************************** + // inputs parameters { + // ParmParse is way of reading inputs from the inputs file + // pp.get means we require the inputs file to have it + // pp.query means we optionally need the inputs file to have it - but we must supply a default here amrex::ParmParse pp; + + // We need to get n_cell from the inputs file - this is the number of cells on each side of + // a square (or cubic) domain. pp.get("n_cell",n_cell); + + // The domain is broken into boxes of size max_grid_size pp.get("max_grid_size",max_grid_size); + + // Default nsteps to 10, allow us to set it to something else in the inputs file nsteps = 10; pp.query("nsteps",nsteps); + + // Default plot_int to -1, allow us to set it to something else in the inputs file + // If plot_int < 0 then no plot files will be written plot_int = -1; pp.query("plot_int",plot_int); + + // time step pp.get("dt",dt); } @@ -52,42 +78,74 @@ SimulationResult heat_equation_main(int argc, char* argv[]) // DEFINE SIMULATION SETUP AND GEOMETRY // ********************************** + // make BoxArray and Geometry + // ba will contain a list of boxes that cover the domain + // geom contains information such as the physical domain size, + // number of points in the domain, and periodicity amrex::BoxArray ba; amrex::Geometry geom; + // define lower and upper indices amrex::IntVect dom_lo(0,0,0); amrex::IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); + // Make a single box that is the entire domain amrex::Box domain(dom_lo, dom_hi); + + // Initialize the boxarray "ba" from the single box "domain" ba.define(domain); + + // Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction ba.maxSize(max_grid_size); - amrex::RealBox real_box({ 0., 0., 0.}, { 1., 1., 1.}); + // This defines the physical box, [0,1] in each direction. + amrex::RealBox real_box({ 0., 0., 0.}, + { 1., 1., 1.}); + + // periodic in all direction amrex::Array is_periodic{1,1,1}; + + // This defines a Geometry object geom.define(domain, real_box, amrex::CoordSys::cartesian, is_periodic); + // extract dx from the geometry object amrex::GpuArray dx = geom.CellSizeArray(); + // Nghost = number of ghost cells for each array int Nghost = 1; + + // Ncomp = number of components for each array int Ncomp = 1; + // How Boxes are distrubuted among MPI processes amrex::DistributionMapping dm(ba); + + // we allocate two phi multifabs; one will store the old state, the other the new. amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); + // time = starting time in the simulation amrex::Real time = 0.0; // ********************************** // INITIALIZE DATA LOOP // ********************************** + // loop over boxes for (amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi) { const amrex::Box& bx = mfi.validbox(); + const amrex::Array4& phiOld = phi_old.array(mfi); + // set phi = 1 + e^(-(r-0.5)^2) amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) { + + // ********************************** + // SET VALUES FOR EACH CELL + // ********************************** + amrex::Real x = (i+0.5) * dx[0]; amrex::Real y = (j+0.5) * dx[1]; amrex::Real z = (k+0.5) * dx[2]; @@ -100,6 +158,7 @@ SimulationResult heat_equation_main(int argc, char* argv[]) // WRITE INITIAL PLOT FILE // ********************************** + // Write a plotfile of the initial data if plot_int > 0 if (plot_int > 0) { int step = 0; @@ -107,22 +166,33 @@ SimulationResult heat_equation_main(int argc, char* argv[]) WriteSingleLevelPlotfile(pltfile, phi_old, {"phi"}, geom, time, 0); } + // ********************************** // MAIN TIME EVOLUTION LOOP // ********************************** for (int step = 1; step <= nsteps; ++step) { + // fill periodic ghost cells phi_old.FillBoundary(geom.periodicity()); + // new_phi = old_phi + dt * Laplacian(old_phi) + // loop over boxes for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ) { const amrex::Box& bx = mfi.validbox(); + const amrex::Array4& phiOld = phi_old.array(mfi); const amrex::Array4& phiNew = phi_new.array(mfi); + // advance the data by dt amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) { + + // ********************************** + // EVOLVE VALUES FOR EACH CELL + // ********************************** + phiNew(i,j,k) = phiOld(i,j,k) + dt * ( (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) @@ -131,15 +201,29 @@ SimulationResult heat_equation_main(int argc, char* argv[]) }); } + // ********************************** + // INCREMENT + // ********************************** + + // update time time = time + dt; + + // copy new solution into old solution amrex::MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); // Update result with current state result.final_step = step; result.final_time = time; + // Tell the I/O Processor to write out which step we're doing amrex::Print() << "Advanced step " << step << "\n"; + + // ********************************** + // WRITE PLOTFILE AT GIVEN INTERVAL + // ********************************** + + // Write a plotfile of the current data (plot_int was defined in the inputs file) if (plot_int > 0 && step%plot_int == 0) { const std::string& pltfile = amrex::Concatenate("plt",step,5); @@ -160,4 +244,6 @@ int main(int argc, char* argv[]) { heat_equation_main(argc, argv); return 0; -} \ No newline at end of file +} + + From c3a18b4e29e8a7bee4091aecb70e23674d9c1bbd Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Wed, 24 Sep 2025 17:36:59 -0700 Subject: [PATCH 011/142] Minimal docs need checking --- Docs/source/HeatEquation_PythonDriver.rst | 326 ++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 Docs/source/HeatEquation_PythonDriver.rst diff --git a/Docs/source/HeatEquation_PythonDriver.rst b/Docs/source/HeatEquation_PythonDriver.rst new file mode 100644 index 00000000..41a32d60 --- /dev/null +++ b/Docs/source/HeatEquation_PythonDriver.rst @@ -0,0 +1,326 @@ +.. _guided_heat_python_driver: + +Tutorial: Heat Equation - Python Driver +======================================== + +.. admonition:: **Time to Complete**: 10 mins + :class: warning + + **PREREQUISITES:** + - Complete :ref:`guided_heat_simple` tutorial first + + **GOALS:** + - Create minimal Python interface to AMReX C++ code + - Implement one-line simulation execution from Python + - Access simulation results programmatically + - Understand the function refactoring pattern + + +This tutorial demonstrates the minimal setup needed to create a Python interface +for AMReX C++ simulation codes. The focus is on a single, clean interface that +allows you to run simulations from Python and access results directly. + +The One-Line Interface +~~~~~~~~~~~~~~~~~~~~~~ + +The goal is to enable this simple Python usage pattern: + +.. code-block:: python + + import amrex_heat + + # One-line simulation execution with structured results + result = amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"]) + + print(f"Max temperature: {result.max_temperature}") + print(f"Final time: {result.final_time}") + print(f"Success: {result.success}") + +This approach provides direct access to simulation results without subprocess overhead +or complex callback systems. + +Key Architecture Pattern +~~~~~~~~~~~~~~~~~~~~~~~~ + +The essential insight is to refactor the C++ ``main()`` function into a reusable +function that can be called from both the command line and Python: + +.. code-block:: cpp + + // Reusable simulation function + SimulationResult heat_equation_main(int argc, char* argv[]) { + amrex::Initialize(argc, argv); + { + // All simulation logic here + // ... + result.max_temperature = phi_new.max(0); + result.success = true; + } + amrex::Finalize(); + return result; + } + + // C++ entry point + int main(int argc, char* argv[]) { + heat_equation_main(argc, argv); + return 0; + } + +This pattern allows the same simulation code to be used from: + +- **Command line**: ``./HeatEquation_PythonDriver inputs`` +- **Python**: ``amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"])`` + +Building the Project +~~~~~~~~~~~~~~~~~~~~ + +Navigate to the directory :code:`amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/` +and build the project: + +.. code-block:: bash + + mkdir build + cd build + cmake .. + cmake --build . -j4 + +This will create both: + +- ``HeatEquation_PythonDriver`` - C++ executable +- ``amrex_heat.cpython-*.so`` - Python module + +Required Files +~~~~~~~~~~~~~~ + +The ``HeatEquation_PythonDriver`` directory contains these essential files: + +- ``main.cpp`` - Heat equation solver with ``heat_equation_main()`` function +- ``bindings.cpp`` - Minimal pybind11 interface exposing the one-liner +- ``CMakeLists.txt`` - Build configuration with pybind11 support +- ``pybind11.cmake`` - Pybind11 infrastructure (copied from pyamrex) +- ``inputs`` - Simulation parameters (``n_cell``, ``dt``, etc.) +- ``test.py`` - Example Python usage script +- ``README.md`` - Documentation and usage instructions + +Implementation Details +~~~~~~~~~~~~~~~~~~~~~~ + +Result Structure +^^^^^^^^^^^^^^^^ + +The simulation returns a simple struct with essential information: + +.. code-block:: cpp + + struct SimulationResult { + double max_temperature; + int final_step; + double final_time; + bool success; + }; + +This struct is automatically exposed to Python through pybind11, allowing direct +access to all fields. + +Minimal Python Bindings +^^^^^^^^^^^^^^^^^^^^^^^^ + +The Python interface is implemented with minimal pybind11 code in ``bindings.cpp``. +The key components are: + +1. **Forward declaration** of the ``SimulationResult`` struct from ``main.cpp`` +2. **Function declaration** for ``heat_equation_main()`` +3. **Pybind11 module** that exposes both the struct and function + +.. code-block:: cpp + + // Forward declarations from main.cpp + struct SimulationResult { + double max_temperature; + int final_step; + double final_time; + bool success; + }; + + SimulationResult heat_equation_main(int argc, char* argv[]); + + PYBIND11_MODULE(amrex_heat, m) { + m.doc() = "Minimal AMReX Heat Equation Python Interface"; + + // Expose SimulationResult struct + py::class_(m, "SimulationResult") + .def_readonly("max_temperature", &SimulationResult::max_temperature) + .def_readonly("final_step", &SimulationResult::final_step) + .def_readonly("final_time", &SimulationResult::final_time) + .def_readonly("success", &SimulationResult::success); + + // Main simulation function - one-liner interface + m.def("run_simulation", [](py::list args) { + // Convert Python list to C++ argc/argv with proper lifetime management + std::vector args_str; + for (auto item : args) { + args_str.push_back(py::str(item)); + } + + std::vector args_cstr; + for (auto& s : args_str) { + args_cstr.push_back(const_cast(s.c_str())); + } + args_cstr.push_back(nullptr); // Null terminate + + return heat_equation_main(static_cast(args_cstr.size() - 1), args_cstr.data()); + }, "Run the heat equation simulation and return results"); + } + +The argument conversion ensures proper lifetime management of the C++ strings and +null-terminates the argument array as expected by ``argc/argv`` conventions. + +CMake Integration +^^^^^^^^^^^^^^^^^ + +The ``CMakeLists.txt`` integrates pybind11 using the pyamrex infrastructure. The key +elements are the pybind11 integration and building both targets from the same source: + +.. code-block:: cmake + + # Use pyamrex pybind11 infrastructure + include(pybind11.cmake) + + # Add the main executable + add_executable(HeatEquation_PythonDriver main.cpp) + + # Add the pybind11 module including main simulation logic + pybind11_add_module(amrex_heat bindings.cpp main.cpp) + + # Link AMReX to both targets + target_link_libraries(HeatEquation_PythonDriver PRIVATE AMReX::amrex) + target_link_libraries(amrex_heat PRIVATE AMReX::amrex) + +The ``pybind11.cmake`` file is copied from the pyamrex repository and provides the necessary +pybind11 infrastructure without requiring a separate pyamrex installation. + +Running the Examples +~~~~~~~~~~~~~~~~~~~~ + +C++ Executable +^^^^^^^^^^^^^^ + +Test the traditional C++ interface: + +.. code-block:: bash + + cd build + ./HeatEquation_PythonDriver inputs + +This runs the simulation and prints progress to the terminal. + +Python Interface +^^^^^^^^^^^^^^^^ + +Test the new Python interface: + +.. code-block:: bash + + cd build + python ../test.py + +The Python script demonstrates accessing simulation results: + +.. code-block:: python + + import amrex_heat + + print("Running simulation...") + result = amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"]) + + print(f"Simulation Results:") + print(f" Success: {result.success}") + print(f" Final step: {result.final_step}") + print(f" Final time: {result.final_time:.6f}") + print(f" Max temperature: {result.max_temperature:.6f}") + +Expected output: + +.. code-block:: + + Heat Equation Python Driver Test + ======================================== + Running simulation... + Advanced step 1 + Advanced step 2 + ... + Advanced step 1000 + + Simulation Results: + Success: True + Final step: 1000 + Final time: 0.010000 + Max temperature: 1.089070 + + ✓ Simulation completed successfully! + +Comparison with Full Implementation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This minimal example focuses on the core one-line interface. The full +:ref:`guided_heat_python_interface` tutorial adds: + +- Callback system for progress monitoring +- Access to simulation data during execution +- More complex return structures +- Real-time interaction capabilities + +The minimal approach shown here provides the foundation that can be extended +as needed for specific applications. + +Benefits of This Approach +~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Simplicity** +^^^^^^^^^^^^^^ +- Only ~50 lines of additional code +- Easy to understand and modify +- Minimal dependencies + +**Performance** +^^^^^^^^^^^^^^^ +- No subprocess overhead +- Direct function calls +- Same performance as C++ executable + +**Flexibility** +^^^^^^^^^^^^^^^ +- Same code for command line and Python +- Easy to extend with more return data +- Foundation for complex workflows + +**Integration** +^^^^^^^^^^^^^^^ +- Works with existing build systems +- Compatible with pyamrex infrastructure +- Follows established patterns from WarpX/Nyx + +Use Cases +~~~~~~~~~ + +This pattern is ideal for: + +- **Parameter sweeps**: Run multiple simulations with different inputs +- **Optimization workflows**: Use simulation results in optimization loops +- **Data analysis pipelines**: Process simulation outputs immediately +- **Jupyter notebooks**: Interactive simulation and visualization +- **Machine learning**: Generate training data or run inference + +Next Steps +~~~~~~~~~~ + +This minimal Python interface provides the foundation for more advanced features: + +1. **Add more return data**: Include arrays, MultiFab statistics, etc. +2. **Parameter setting**: Allow modification of simulation parameters from Python +3. **Progress monitoring**: Add callback system for real-time updates +4. **Full pyamrex integration**: Access MultiFab data structures directly +5. **Workflow automation**: Build complex simulation pipelines + +The key insight is that this simple pattern scales naturally to support more +complex use cases while maintaining the clean one-line interface. \ No newline at end of file From 630ddfdfe666caf5a68f6591864ba2df55b3f6d0 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Wed, 24 Sep 2025 17:44:26 -0700 Subject: [PATCH 012/142] Add more specific steps --- Docs/source/HeatEquation_PythonDriver.rst | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Docs/source/HeatEquation_PythonDriver.rst b/Docs/source/HeatEquation_PythonDriver.rst index 41a32d60..1a1b5e56 100644 --- a/Docs/source/HeatEquation_PythonDriver.rst +++ b/Docs/source/HeatEquation_PythonDriver.rst @@ -321,6 +321,33 @@ This minimal Python interface provides the foundation for more advanced features 3. **Progress monitoring**: Add callback system for real-time updates 4. **Full pyamrex integration**: Access MultiFab data structures directly 5. **Workflow automation**: Build complex simulation pipelines +6. **Generic naming**: Replace heat equation-specific names (``amrex_heat``, ``max_temperature``) with generic equivalents (``amrex_sim``, ``max_value``) for reusability across different simulation types +7. **Numpy-compatible results**: Add options to return data as dictionaries, numpy arrays, or other formats that integrate well with the scientific Python ecosystem + +Potential improvements for generic usage: + +.. code-block:: cpp + + // Generic module and function names + PYBIND11_MODULE(amrex_sim, m) { + // Option 1: Return as dictionary for numpy compatibility + m.def("run_dict", [](py::list args) { + auto result = simulation_main(argc, argv); + py::dict d; + d["success"] = result.success; + d["max_value"] = result.max_value; // Generic field name + d["final_time"] = result.final_time; + return d; + }); + + // Option 2: Return numerical data as numpy array + m.def("run_array", [](py::list args) { + auto result = simulation_main(argc, argv); + py::array_t data = py::array_t(3); + // Fill array with [final_step, final_time, max_value] + return py::make_tuple(result.success, data); + }); + } The key insight is that this simple pattern scales naturally to support more complex use cases while maintaining the clean one-line interface. \ No newline at end of file From fcf1f8854661200c4e74d35df7567fb6f86c1cd8 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Wed, 24 Sep 2025 17:46:18 -0700 Subject: [PATCH 013/142] Add more instructions aka recipe --- Docs/source/HeatEquation_PythonDriver.rst | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/Docs/source/HeatEquation_PythonDriver.rst b/Docs/source/HeatEquation_PythonDriver.rst index 1a1b5e56..2f8ff64a 100644 --- a/Docs/source/HeatEquation_PythonDriver.rst +++ b/Docs/source/HeatEquation_PythonDriver.rst @@ -259,19 +259,24 @@ Expected output: ✓ Simulation completed successfully! -Comparison with Full Implementation +Adapting This Pattern to Your Code ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This minimal example focuses on the core one-line interface. The full -:ref:`guided_heat_python_interface` tutorial adds: +To apply this pattern to your own AMReX simulation, follow these steps: -- Callback system for progress monitoring -- Access to simulation data during execution -- More complex return structures -- Real-time interaction capabilities +1. **Choose your output data**: Decide what simulation results to return (max values, final state, convergence info, etc.) -The minimal approach shown here provides the foundation that can be extended -as needed for specific applications. +2. **Choose return format**: Select struct, dictionary, or numpy array based on your Python workflow needs + +3. **Create the pybind11 module**: Write ``bindings.cpp`` that exposes your chosen interface + +4. **Wrap existing main**: Refactor your ``main()`` function into a reusable function like ``simulation_main()`` or ``your_code_main()`` + +5. **Create Python test**: Write a test script to exercise the new interface + +6. **Update CMake**: Add pybind11 library target and include ``pybind11.cmake`` + +This systematic approach ensures you maintain your existing C++ code while adding clean Python access. Benefits of This Approach ~~~~~~~~~~~~~~~~~~~~~~~~~ From 764c2df925df130ef21625d3e314167c7e6fab8a Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Wed, 24 Sep 2025 17:52:01 -0700 Subject: [PATCH 014/142] Add notes on missing needed --- Docs/source/HeatEquation_PythonDriver.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Docs/source/HeatEquation_PythonDriver.rst b/Docs/source/HeatEquation_PythonDriver.rst index 2f8ff64a..ead3e308 100644 --- a/Docs/source/HeatEquation_PythonDriver.rst +++ b/Docs/source/HeatEquation_PythonDriver.rst @@ -264,15 +264,15 @@ Adapting This Pattern to Your Code To apply this pattern to your own AMReX simulation, follow these steps: -1. **Choose your output data**: Decide what simulation results to return (max values, final state, convergence info, etc.) +1. **Choose your output data**: Decide what simulation results to return (max values, final state, convergence info, etc.) *[Details to be added]* -2. **Choose return format**: Select struct, dictionary, or numpy array based on your Python workflow needs +2. **Choose return format**: Select struct, dictionary, or numpy array based on your Python workflow needs *[Details to be added]* 3. **Create the pybind11 module**: Write ``bindings.cpp`` that exposes your chosen interface 4. **Wrap existing main**: Refactor your ``main()`` function into a reusable function like ``simulation_main()`` or ``your_code_main()`` -5. **Create Python test**: Write a test script to exercise the new interface +5. **Create Python test**: Write a test script to exercise the new interface *[More guidance to be added]* 6. **Update CMake**: Add pybind11 library target and include ``pybind11.cmake`` From 82f1b2cf0335b3bfe592288b76eb8c07a5fe59f3 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 25 Sep 2025 06:42:13 -0700 Subject: [PATCH 015/142] Move HeatEquationModel to Case-2 --- .../Case-2}/HeatEquationModel.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename GuidedTutorials/{HeatEquation/Source => HeatEquation_PythonDriver/Case-2}/HeatEquationModel.py (100%) diff --git a/GuidedTutorials/HeatEquation/Source/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py similarity index 100% rename from GuidedTutorials/HeatEquation/Source/HeatEquationModel.py rename to GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py From 6eb5a1061adcc5b2ab25de2f6d32f78b6a0899ff Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 25 Sep 2025 07:45:13 -0700 Subject: [PATCH 016/142] Move to Case-1 --- .../HeatEquation_PythonDriver/{ => Case-1}/CMakeLists.txt | 0 .../HeatEquation_PythonDriver/{ => Case-1}/bindings.cpp | 0 GuidedTutorials/HeatEquation_PythonDriver/{ => Case-1}/inputs | 0 GuidedTutorials/HeatEquation_PythonDriver/{ => Case-1}/main.cpp | 0 .../HeatEquation_PythonDriver/{ => Case-1}/pybind11.cmake | 0 GuidedTutorials/HeatEquation_PythonDriver/{ => Case-1}/test.py | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-1}/CMakeLists.txt (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-1}/bindings.cpp (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-1}/inputs (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-1}/main.cpp (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-1}/pybind11.cmake (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-1}/test.py (100%) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/CMakeLists.txt b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/CMakeLists.txt similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/CMakeLists.txt rename to GuidedTutorials/HeatEquation_PythonDriver/Case-1/CMakeLists.txt diff --git a/GuidedTutorials/HeatEquation_PythonDriver/bindings.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.cpp similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/bindings.cpp rename to GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.cpp diff --git a/GuidedTutorials/HeatEquation_PythonDriver/inputs b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/inputs rename to GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs diff --git a/GuidedTutorials/HeatEquation_PythonDriver/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/main.cpp rename to GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp diff --git a/GuidedTutorials/HeatEquation_PythonDriver/pybind11.cmake b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/pybind11.cmake similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/pybind11.cmake rename to GuidedTutorials/HeatEquation_PythonDriver/Case-1/pybind11.cmake diff --git a/GuidedTutorials/HeatEquation_PythonDriver/test.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/test.py similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/test.py rename to GuidedTutorials/HeatEquation_PythonDriver/Case-1/test.py From 4ff9f1f1d9f7601187e790c90e764510df9a0909 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 25 Sep 2025 07:45:33 -0700 Subject: [PATCH 017/142] Add GNUmake path for python example based on warpx --- .../Case-1/GNUmakefile | 104 ++++++++++++++++++ .../Case-1/Make.package | 6 + 2 files changed, 110 insertions(+) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile new file mode 100644 index 00000000..409fe08e --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile @@ -0,0 +1,104 @@ +# AMREX_HOME defines the directory in which we will find all the AMReX code. +AMREX_HOME ?= ../../../../amrex + +# For Python support, clone pybind11 as sibling to amrex: +# git clone -b v3.0.0 https://github.com/pybind/pybind11.git +# Or set PYBIND11_HOME to point to your pybind11 installation +PYBIND11_HOME ?= ../../../../pybind11 + +DEBUG = FALSE +USE_MPI = TRUE +USE_OMP = FALSE +COMP = gcc +DIM = 3 + +USE_CUDA = FALSE +USE_HIP = FALSE +USE_SYCL = FALSE + +# Python main control flag +ifndef USE_PYTHON_MAIN + USE_PYTHON_MAIN = FALSE +endif + +include $(AMREX_HOME)/Tools/GNUMake/Make.defs + +# Add pybind11 and Python includes when building for Python +ifeq ($(USE_PYTHON_MAIN),TRUE) + INCLUDE_LOCATIONS += $(PYBIND11_HOME)/include + + # Add Python headers + PYTHON_CONFIG ?= python3-config + CXXFLAGS += $(shell $(PYTHON_CONFIG) --includes) +endif + +# Add PIC flag when building for Python +ifeq ($(USE_PYTHON_MAIN),TRUE) + XTRA_CXXFLAGS += -fPIC +ifeq ($(USE_OMP),TRUE) + LDFLAGS += -fopenmp +endif +endif + +include Make.package +VPATH_LOCATIONS += . +INCLUDE_LOCATIONS += . + +include $(AMREX_HOME)/Src/Base/Make.package + +# Job info support +CEXE_sources += AMReX_buildInfo.cpp +INCLUDE_LOCATIONS += $(AMREX_HOME)/Tools/C_scripts + +include $(AMREX_HOME)/Tools/GNUMake/Make.rules + +# Python library building (using pybind11 naming convention) +ifeq ($(USE_PYTHON_MAIN),TRUE) + +# In WarpX this is machine-dependent +SHARED_OPTION = -shared + +# Pybind11-style naming +PYMODULE_NAME = amrex_heat +PYDIM = $(DIM)d + +installamrex_heat: lib$(PYMODULE_NAME).$(PYDIM).so + cp lib$(PYMODULE_NAME).$(PYDIM).so Python/py$(PYMODULE_NAME) + cd Python; python3 setup.py install --force --with-lib$(PYMODULE_NAME) $(PYDIM) $(PYINSTALLOPTIONS) + +lib$(PYMODULE_NAME).$(PYDIM).a: $(objForExecs) + @echo Making static library $@ ... + $(SILENT) $(AR) -crv $@ $^ + $(SILENT) $(RM) AMReX_buildInfo.cpp + @echo SUCCESS + +lib$(PYMODULE_NAME).$(PYDIM).so: $(objForExecs) + @echo Making dynamic library $@ ... +ifeq ($(USE_CUDA),TRUE) + $(SILENT) $(CXX) $(LINKFLAGS) $(SHARED_OPTION) -Xlinker=$(SHARED_OPTION) -Xlinker=-fPIC -o $@ $^ $(LDFLAGS) $(libraries) +else + $(SILENT) $(CXX) $(LINKFLAGS) $(SHARED_OPTION) -fPIC -o $@ $^ $(LDFLAGS) $(libraries) +endif + $(SILENT) $(RM) AMReX_buildInfo.cpp + @echo SUCCESS + +clean:: + $(SILENT) $(RM) -rf build + $(SILENT) $(RM) -f lib$(PYMODULE_NAME).?d.a + $(SILENT) $(RM) -f lib$(PYMODULE_NAME).?d.so + $(SILENT) $(RM) -f py$(PYMODULE_NAME)/lib$(PYMODULE_NAME).?d.so + $(SILENT) $(RM) -f $(PYMODULE_NAME).*.so # Clean pybind11-style names too + +endif + +# Convenience targets +.PHONY: python_lib python_install inputs + +python_lib: + $(MAKE) USE_PYTHON_MAIN=TRUE lib$(PYMODULE_NAME).$(DIM)d.so + +python_install: + $(MAKE) USE_PYTHON_MAIN=TRUE installamrex_heat + +inputs: + cp -r inputs/* . diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package new file mode 100644 index 00000000..21ecf512 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package @@ -0,0 +1,6 @@ +CEXE_sources += main.cpp + +# Add bindings when building for Python +ifeq ($(USE_PYTHON_MAIN),TRUE) + CEXE_sources += bindings.cpp +endif \ No newline at end of file From 149a6486a58e39ac70589dec8c591d0462c05c2a Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 25 Sep 2025 08:57:34 -0700 Subject: [PATCH 018/142] GNUmake still not setting up link and targets correctly --- .../Case-1/GNUmakefile | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile index 409fe08e..e4df1d85 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile @@ -28,8 +28,17 @@ ifeq ($(USE_PYTHON_MAIN),TRUE) INCLUDE_LOCATIONS += $(PYBIND11_HOME)/include # Add Python headers - PYTHON_CONFIG ?= python3-config - CXXFLAGS += $(shell $(PYTHON_CONFIG) --includes) + #PYTHON_CONFIG ?= python3-config + + # Add Python includes + #PYTHON_INCLUDES := $(shell $(PYTHON_CONFIG) --includes) + #CXXFLAGS += $(PYTHON_INCLUDES) + + # Add Python linking flags - try both methods + #PYTHON_LDFLAGS := $(shell $(PYTHON_CONFIG) --ldflags --embed 2>/dev/null || $(PYTHON_CONFIG) --ldflags) + #PYTHON_LIBS := $(shell $(PYTHON_CONFIG) --libs --embed 2>/dev/null || $(PYTHON_CONFIG) --libs) + + #LDFLAGS += $(PYTHON_LDFLAGS) $(PYTHON_LIBS) endif # Add PIC flag when building for Python @@ -46,15 +55,22 @@ INCLUDE_LOCATIONS += . include $(AMREX_HOME)/Src/Base/Make.package -# Job info support -CEXE_sources += AMReX_buildInfo.cpp -INCLUDE_LOCATIONS += $(AMREX_HOME)/Tools/C_scripts - include $(AMREX_HOME)/Tools/GNUMake/Make.rules +# Python library building (using pybind11 naming convention) # Python library building (using pybind11 naming convention) ifeq ($(USE_PYTHON_MAIN),TRUE) +# Add pybind11 and Python configuration +INCLUDE_LOCATIONS += $(PYBIND11_HOME)/include +PYTHON_CONFIG ?= python3-config +CXXFLAGS += $(shell $(PYTHON_CONFIG) --includes) +CXXFLAGS += -fPIC + +# Python linking flags for the shared library +PYTHON_LDFLAGS := $(shell $(PYTHON_CONFIG) --ldflags --embed 2>/dev/null || $(PYTHON_CONFIG) --ldflags) +PYTHON_LIBS := $(shell $(PYTHON_CONFIG) --libs --embed 2>/dev/null || $(PYTHON_CONFIG) --libs) + # In WarpX this is machine-dependent SHARED_OPTION = -shared @@ -62,6 +78,9 @@ SHARED_OPTION = -shared PYMODULE_NAME = amrex_heat PYDIM = $(DIM)d +# Build both main executable and Python library +all: $(main_exe) lib$(PYMODULE_NAME).$(PYDIM).so + installamrex_heat: lib$(PYMODULE_NAME).$(PYDIM).so cp lib$(PYMODULE_NAME).$(PYDIM).so Python/py$(PYMODULE_NAME) cd Python; python3 setup.py install --force --with-lib$(PYMODULE_NAME) $(PYDIM) $(PYINSTALLOPTIONS) @@ -75,9 +94,9 @@ lib$(PYMODULE_NAME).$(PYDIM).a: $(objForExecs) lib$(PYMODULE_NAME).$(PYDIM).so: $(objForExecs) @echo Making dynamic library $@ ... ifeq ($(USE_CUDA),TRUE) - $(SILENT) $(CXX) $(LINKFLAGS) $(SHARED_OPTION) -Xlinker=$(SHARED_OPTION) -Xlinker=-fPIC -o $@ $^ $(LDFLAGS) $(libraries) + $(SILENT) $(CXX) $(LINKFLAGS) $(SHARED_OPTION) -Xlinker=$(SHARED_OPTION) -Xlinker=-fPIC -o $@ $^ $(LDFLAGS) $(libraries) $(PYTHON_LDFLAGS) $(PYTHON_LIBS) else - $(SILENT) $(CXX) $(LINKFLAGS) $(SHARED_OPTION) -fPIC -o $@ $^ $(LDFLAGS) $(libraries) + $(SILENT) $(CXX) $(LINKFLAGS) $(SHARED_OPTION) -fPIC -o $@ $^ $(LDFLAGS) $(libraries) $(PYTHON_LDFLAGS) $(PYTHON_LIBS) endif $(SILENT) $(RM) AMReX_buildInfo.cpp @echo SUCCESS @@ -89,6 +108,9 @@ clean:: $(SILENT) $(RM) -f py$(PYMODULE_NAME)/lib$(PYMODULE_NAME).?d.so $(SILENT) $(RM) -f $(PYMODULE_NAME).*.so # Clean pybind11-style names too +else +# When USE_PYTHON_MAIN=FALSE, only build main executable +# (existing AMReX build system handles this) endif # Convenience targets From 0c34ac83ab4e7c9f3ea7874ce97876a1c3bd2084 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 25 Sep 2025 09:16:46 -0700 Subject: [PATCH 019/142] tweak readme --- GuidedTutorials/HeatEquation_PythonDriver/{ => Case-1}/README.md | 0 GuidedTutorials/HeatEquation_PythonDriver/Case-2/README.md | 1 + 2 files changed, 1 insertion(+) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-1}/README.md (100%) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-2/README.md diff --git a/GuidedTutorials/HeatEquation_PythonDriver/README.md b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/README.md similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/README.md rename to GuidedTutorials/HeatEquation_PythonDriver/Case-1/README.md diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/README.md b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/README.md new file mode 100644 index 00000000..f328b031 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/README.md @@ -0,0 +1 @@ +This is an example of an object oriented approach creating a HeatEquationModel object in python that wraps the contents of main.py from the GuidedTutorials/HeatEquation/Source example \ No newline at end of file From 7f9388696b10576e61d9d0623135edddb08414c9 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 25 Sep 2025 09:17:34 -0700 Subject: [PATCH 020/142] Minor cleanup --- .../Case-2/HeatEquationModel.py | 37 ------------------- .../HeatEquation_PythonDriver/Case-2/inputs | 7 ++++ 2 files changed, 7 insertions(+), 37 deletions(-) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py index a8756dda..c056ca03 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py @@ -347,43 +347,6 @@ def get_outnames(self): """ return ["max", "mean", "std", "integral", "center"] -class IshigamiSurrogate: - """Black box model that might be a surrogate for Ishigami.""" - - def __call__(self, params): - """ - Run equations for each parameter set. - - Parameters: - ----------- - params : numpy.ndarray of shape (n_samples, 3) - params[:, 0] = x1 - params[:, 1] = x2 - params[:, 2] = x3 - - Returns: - -------- - numpy.ndarray of shape (n_samples, 5) for each sample - """ - if params.ndim == 1: - params = params.reshape(1, -1) - - n_samples = params.shape[0] - outputs = np.zeros((n_samples, 5)) - - for i in range(n_samples): - x1 = params[i, 0] - x2 = params[i, 1] - x3 = params[i, 2] - - outputs[i, 0] = np.exp(x1) - outputs[i, 1] = x3 * np.log(x1**2) + x1 + (x2**2) * (1 - np.exp(-x3**2)) - outputs[i, 2] = x1 + x3**2 - outputs[i, 3] = x2 + x3 - outputs[i, 4] = x1 * x3 - - return outputs - if __name__ == "__main__": # Initialize AMReX diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs new file mode 100644 index 00000000..5b098f82 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs @@ -0,0 +1,7 @@ +n_cell = 32 +max_grid_size = 16 + +nsteps = 1000 +plot_int = 100 + +dt = 1.e-5 From e321e72132f293149a8065570f5cadd266ffe62b Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 25 Sep 2025 09:32:05 -0700 Subject: [PATCH 021/142] Tweak docs for cases --- Docs/source/HeatEquation_PythonDriver.rst | 187 ++++++++++++++-------- 1 file changed, 122 insertions(+), 65 deletions(-) diff --git a/Docs/source/HeatEquation_PythonDriver.rst b/Docs/source/HeatEquation_PythonDriver.rst index ead3e308..b8ac9d5e 100644 --- a/Docs/source/HeatEquation_PythonDriver.rst +++ b/Docs/source/HeatEquation_PythonDriver.rst @@ -16,9 +16,13 @@ Tutorial: Heat Equation - Python Driver - Understand the function refactoring pattern -This tutorial demonstrates the minimal setup needed to create a Python interface -for AMReX C++ simulation codes. The focus is on a single, clean interface that -allows you to run simulations from Python and access results directly. +This tutorial demonstrates two complementary approaches for creating Python interfaces +to AMReX C++ simulation codes: + +- **Case-1**: Minimal pybind11 interface with one-line simulation execution +- **Case-2**: Pure Python approach using pyamrex MultiFabs directly + +Both approaches can handle complex simulations and MultiFab data structures. The One-Line Interface ~~~~~~~~~~~~~~~~~~~~~~ @@ -71,37 +75,65 @@ This pattern allows the same simulation code to be used from: - **Command line**: ``./HeatEquation_PythonDriver inputs`` - **Python**: ``amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"])`` -Building the Project -~~~~~~~~~~~~~~~~~~~~ +Two Implementation Approaches +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Navigate to the directory :code:`amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/` -and build the project: +Case-1: Minimal pybind11 Interface +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Location: :code:`amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/Case-1/` + +This approach creates C++ bindings using pybind11 for direct simulation execution: .. code-block:: bash - mkdir build - cd build + cd Case-1 + mkdir build && cd build cmake .. cmake --build . -j4 -This will create both: - +Creates both: - ``HeatEquation_PythonDriver`` - C++ executable - ``amrex_heat.cpython-*.so`` - Python module -Required Files -~~~~~~~~~~~~~~ +.. note:: + The GNUmakefile in Case-1 is experimental and under development as an alternative build system. + +Case-2: Pure Python with pyamrex +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Location: :code:`amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/Case-2/` + +This approach uses pure Python with pyamrex to access MultiFabs and AMReX functionality directly: + +.. code-block:: bash + + cd Case-2 + python HeatEquationModel.py -The ``HeatEquation_PythonDriver`` directory contains these essential files: +Case-1 Files (pybind11 Approach) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``Case-1`` directory contains: - ``main.cpp`` - Heat equation solver with ``heat_equation_main()`` function - ``bindings.cpp`` - Minimal pybind11 interface exposing the one-liner - ``CMakeLists.txt`` - Build configuration with pybind11 support +- ``GNUmakefile`` - Experimental GNU Make build system (under development) - ``pybind11.cmake`` - Pybind11 infrastructure (copied from pyamrex) - ``inputs`` - Simulation parameters (``n_cell``, ``dt``, etc.) - ``test.py`` - Example Python usage script - ``README.md`` - Documentation and usage instructions +Case-2 Files (Pure Python Approach) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ``Case-2`` directory contains: + +- ``HeatEquationModel.py`` - Pure Python implementation using pyamrex MultiFabs +- ``inputs`` - Simulation parameters +- ``README.md`` - Documentation for the pure Python approach + Implementation Details ~~~~~~~~~~~~~~~~~~~~~~ @@ -202,29 +234,34 @@ pybind11 infrastructure without requiring a separate pyamrex installation. Running the Examples ~~~~~~~~~~~~~~~~~~~~ -C++ Executable -^^^^^^^^^^^^^^ +Case-1: pybind11 Interface +^^^^^^^^^^^^^^^^^^^^^^^^^^ -Test the traditional C++ interface: +**C++ Executable:** .. code-block:: bash - cd build + cd Case-1/build ./HeatEquation_PythonDriver inputs -This runs the simulation and prints progress to the terminal. +**Python Interface:** -Python Interface -^^^^^^^^^^^^^^^^ +.. code-block:: bash -Test the new Python interface: + cd Case-1/build + python ../test.py + +Case-2: Pure Python with pyamrex +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Pure Python Execution:** .. code-block:: bash - cd build - python ../test.py + cd Case-2 + python HeatEquationModel.py -The Python script demonstrates accessing simulation results: +The Case-1 Python script demonstrates accessing simulation results: .. code-block:: python @@ -259,62 +296,82 @@ Expected output: ✓ Simulation completed successfully! -Adapting This Pattern to Your Code -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Choosing the Right Approach +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +**Use Case-1 (pybind11) when:** +- You want to create custom bindings for existing C++ code +- You need a one-line interface wrapping complex C++ simulation logic +- You want both command-line and Python interfaces from the same codebase +- You prefer minimal changes to existing C++ code + +**Use Case-2 (Pure Python with pyamrex) when:** +- You want to write simulation logic directly in Python +- You prefer leveraging the full pyamrex ecosystem +- You want object-oriented simulation management +- You need rapid prototyping and development -To apply this pattern to your own AMReX simulation, follow these steps: +Adapting These Patterns +^^^^^^^^^^^^^^^^^^^^^^^ -1. **Choose your output data**: Decide what simulation results to return (max values, final state, convergence info, etc.) *[Details to be added]* +**For Case-1 (pybind11 approach):** -2. **Choose return format**: Select struct, dictionary, or numpy array based on your Python workflow needs *[Details to be added]* +1. **Refactor main()**: Extract simulation logic into reusable function +2. **Design result structure**: Choose what data to return to Python +3. **Create bindings**: Write ``bindings.cpp`` for your interface +4. **Update build**: Add pybind11 support to CMakeLists.txt -3. **Create the pybind11 module**: Write ``bindings.cpp`` that exposes your chosen interface +**For Case-2 (Pure Python approach):** -4. **Wrap existing main**: Refactor your ``main()`` function into a reusable function like ``simulation_main()`` or ``your_code_main()`` +1. **Design class interface**: Define methods for initialization, execution, results +2. **Use pyamrex directly**: Leverage MultiFabs and AMReX functionality +3. **Implement simulation logic**: Write algorithm using pyamrex primitives +4. **Add data management**: Handle input/output and state management -5. **Create Python test**: Write a test script to exercise the new interface *[More guidance to be added]* +Benefits Comparison +~~~~~~~~~~~~~~~~~~~ -6. **Update CMake**: Add pybind11 library target and include ``pybind11.cmake`` +**Case-1 (pybind11) Benefits:** -This systematic approach ensures you maintain your existing C++ code while adding clean Python access. +- **Minimal C++ changes**: Preserves existing simulation logic +- **Performance**: Direct C++ calls with no overhead +- **Dual interface**: Same code for command line and Python +- **Integration**: Works with existing build systems -Benefits of This Approach -~~~~~~~~~~~~~~~~~~~~~~~~~ +**Case-2 (Pure Python) Benefits:** -**Simplicity** -^^^^^^^^^^^^^^ -- Only ~50 lines of additional code -- Easy to understand and modify -- Minimal dependencies +- **Development speed**: Rapid iteration and testing +- **Ecosystem access**: Full pyamrex functionality available +- **Readability**: Clear Python simulation logic +- **Flexibility**: Easy to modify and extend algorithms -**Performance** -^^^^^^^^^^^^^^^ -- No subprocess overhead -- Direct function calls -- Same performance as C++ executable +**Both approaches:** -**Flexibility** -^^^^^^^^^^^^^^^ -- Same code for command line and Python -- Easy to extend with more return data -- Foundation for complex workflows +- Support complex MultiFab operations +- Enable sophisticated simulation workflows +- Integrate well with scientific Python ecosystem +- Provide foundation for advanced features -**Integration** -^^^^^^^^^^^^^^^ -- Works with existing build systems -- Compatible with pyamrex infrastructure -- Follows established patterns from WarpX/Nyx +Use Cases for Each Approach +~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Use Cases -~~~~~~~~~ +**Case-1 (pybind11) is ideal for:** +- **Wrapping existing C++ codes**: Minimal changes to proven simulation codes +- **Performance-critical workflows**: Direct C++ execution with minimal overhead +- **One-line interfaces**: Simple Python access to complex simulations +- **Hybrid development**: Teams with both C++ and Python expertise -This pattern is ideal for: +**Case-2 (Pure Python with pyamrex) is ideal for:** +- **Rapid prototyping**: Quick iteration on simulation algorithms +- **Educational purposes**: Clear, readable simulation logic +- **Python-first development**: Teams primarily working in Python +- **Leveraging pyamrex ecosystem**: Using existing pyamrex tools and patterns -- **Parameter sweeps**: Run multiple simulations with different inputs -- **Optimization workflows**: Use simulation results in optimization loops -- **Data analysis pipelines**: Process simulation outputs immediately -- **Jupyter notebooks**: Interactive simulation and visualization -- **Machine learning**: Generate training data or run inference +**Both approaches support:** +- Parameter sweeps and optimization workflows +- Jupyter notebooks and interactive visualization +- Complex MultiFab operations and data analysis +- Machine learning and data science pipelines Next Steps ~~~~~~~~~~ From 57d4336778083668d8c4753c726cb569fc367cfc Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 25 Sep 2025 09:35:18 -0700 Subject: [PATCH 022/142] Tweak docs for examples --- Docs/source/HeatEquation_PythonDriver.rst | 32 ++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/Docs/source/HeatEquation_PythonDriver.rst b/Docs/source/HeatEquation_PythonDriver.rst index b8ac9d5e..228519c6 100644 --- a/Docs/source/HeatEquation_PythonDriver.rst +++ b/Docs/source/HeatEquation_PythonDriver.rst @@ -134,8 +134,8 @@ The ``Case-2`` directory contains: - ``inputs`` - Simulation parameters - ``README.md`` - Documentation for the pure Python approach -Implementation Details -~~~~~~~~~~~~~~~~~~~~~~ +Case-1 Implementation Details +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Result Structure ^^^^^^^^^^^^^^^^ @@ -154,8 +154,8 @@ The simulation returns a simple struct with essential information: This struct is automatically exposed to Python through pybind11, allowing direct access to all fields. -Minimal Python Bindings -^^^^^^^^^^^^^^^^^^^^^^^^ + Minimal Python Bindings + ++++++++++++++++++++++++ The Python interface is implemented with minimal pybind11 code in ``bindings.cpp``. The key components are: @@ -207,8 +207,8 @@ The key components are: The argument conversion ensures proper lifetime management of the C++ strings and null-terminates the argument array as expected by ``argc/argv`` conventions. -CMake Integration -^^^^^^^^^^^^^^^^^ + CMake Integration + +++++++++++++++++ The ``CMakeLists.txt`` integrates pybind11 using the pyamrex infrastructure. The key elements are the pybind11 integration and building both targets from the same source: @@ -296,6 +296,26 @@ Expected output: ✓ Simulation completed successfully! +Case-2 Example Output +^^^^^^^^^^^^^^^^^^^^^ + +The pure Python approach provides object-oriented access: + +.. code-block:: python + + import numpy as np + from HeatEquationModel import HeatEquationModel + + # Create model using inputs file + model = HeatEquationModel(use_parmparse=True) + + # Run with parameter array [diffusion_coeff, init_amplitude, init_width] + params = np.array([1.0, 1.0, 0.01]) + results = model(params) + + print(f"Results: {results}") + # Output: [max_value, mean_value, std_dev, total_heat, center_value] + Choosing the Right Approach ~~~~~~~~~~~~~~~~~~~~~~~~~~~ From ce7703d382e91e35d88011fb184a78541691c200 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 25 Sep 2025 09:48:43 -0700 Subject: [PATCH 023/142] Tweak docs for generic needs more organizing --- Docs/source/HeatEquation_PythonDriver.rst | 52 +++++++++++++++-------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/Docs/source/HeatEquation_PythonDriver.rst b/Docs/source/HeatEquation_PythonDriver.rst index 228519c6..8fb99945 100644 --- a/Docs/source/HeatEquation_PythonDriver.rst +++ b/Docs/source/HeatEquation_PythonDriver.rst @@ -27,53 +27,71 @@ Both approaches can handle complex simulations and MultiFab data structures. The One-Line Interface ~~~~~~~~~~~~~~~~~~~~~~ -The goal is to enable this simple Python usage pattern: +The goal is to enable simple Python usage patterns that work across different simulation types: + +**Case-1 Pattern (pybind11 C++ interface):** .. code-block:: python - import amrex_heat + import amrex_sim # One-line simulation execution with structured results - result = amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"]) + result = amrex_sim.run_simulation(["./YourSimulation", "inputs"]) - print(f"Max temperature: {result.max_temperature}") + print(f"Max value: {result.max_value}") print(f"Final time: {result.final_time}") print(f"Success: {result.success}") -This approach provides direct access to simulation results without subprocess overhead +**Case-2 Pattern (Pure Python with pyamrex):** + +.. code-block:: python + + from YourModel import SimulationModel + + # One-line execution with parameter arrays + model = SimulationModel(use_parmparse=True) + results = model(params) # Returns numpy array of results + + print(f"Results: {results}") # [max, mean, std, integral, center] + +Both approaches provide direct access to simulation results without subprocess overhead or complex callback systems. Key Architecture Pattern ~~~~~~~~~~~~~~~~~~~~~~~~ -The essential insight is to refactor the C++ ``main()`` function into a reusable -function that can be called from both the command line and Python: +Both approaches demonstrate the core principle of creating **simplified interfaces** to complex AMReX simulations. The key insight is to transform existing simulation logic into easily callable functions with structured return values: + +**Case-1 Approach (C++ Refactoring):** +Refactor the C++ ``main()`` function into a reusable function that can be called from both command line and Python: .. code-block:: cpp // Reusable simulation function - SimulationResult heat_equation_main(int argc, char* argv[]) { + SimulationResult simulation_main(int argc, char* argv[]) { amrex::Initialize(argc, argv); { // All simulation logic here // ... - result.max_temperature = phi_new.max(0); + result.max_value = multifab.max(0); result.success = true; } amrex::Finalize(); return result; } - // C++ entry point - int main(int argc, char* argv[]) { - heat_equation_main(argc, argv); - return 0; - } +**Case-2 Approach (Python Function Design):** +Create a callable class or function that encapsulates simulation parameters and execution: + +.. code-block:: python -This pattern allows the same simulation code to be used from: + class SimulationModel: + def __call__(self, params): + # All simulation logic using pyamrex + # ... + return np.array([max_val, mean_val, std_val, integral, center_val]) -- **Command line**: ``./HeatEquation_PythonDriver inputs`` -- **Python**: ``amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"])`` +Both patterns enable the same core benefit: **one-line simulation execution** with structured results, regardless of whether the underlying implementation uses C++ bindings or pure Python. Two Implementation Approaches ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 8c67878694c65ad931cbc078ed2061ef974acd26 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 08:31:09 -0700 Subject: [PATCH 024/142] First pass at modularizing --- .../Case-2/HeatEquationModel.py | 299 +++--------------- .../HeatEquation_PythonDriver/Case-2/main.py | 237 ++++++++++++++ 2 files changed, 273 insertions(+), 263 deletions(-) create mode 100755 GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py index c056ca03..53cc8d26 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py @@ -5,201 +5,52 @@ import numpy as np -def load_cupy(): - """Load GPU backend if available.""" - if amr.Config.have_gpu: - try: - import cupy as cp - xp = cp - amr.Print("Note: found and will use cupy") - except ImportError: - amr.Print("Warning: GPU found but cupy not available! Trying managed memory in numpy...") - import numpy as np - xp = np - if amr.Config.gpu_backend == "SYCL": - amr.Print("Warning: SYCL GPU backend not yet implemented for Python") - import numpy as np - xp = np - - else: - import numpy as np - xp = np - amr.Print("Note: found and will use numpy") - return xp - - -def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, - n_cell=32, max_grid_size=16, nsteps=100, plot_int=100, dt=1e-5): - """ - Run heat equation with given parameters and return final state metrics. - - Returns: [max_value, mean_value, std_dev, total_heat, center_value] - """ - plot_files_output = False - # CPU/GPU logic - xp = load_cupy() - - # AMREX_D_DECL means "do the first X of these, where X is the dimensionality of the simulation" - dom_lo = amr.IntVect(*amr.d_decl( 0, 0, 0)) - dom_hi = amr.IntVect(*amr.d_decl(n_cell-1, n_cell-1, n_cell-1)) - - # Make a single box that is the entire domain - domain = amr.Box(dom_lo, dom_hi) - - # Make BoxArray and Geometry: - # ba contains a list of boxes that cover the domain, - # geom contains information such as the physical domain size, - # number of points in the domain, and periodicity - - # Initialize the boxarray "ba" from the single box "domain" - ba = amr.BoxArray(domain) - # Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction - ba.max_size(max_grid_size) - - # This defines the physical box, [0,1] in each direction. - real_box = amr.RealBox([*amr.d_decl( 0., 0., 0.)], [*amr.d_decl( 1., 1., 1.)]) - - # This defines a Geometry object - # periodic in all direction - coord = 0 # Cartesian - is_per = [*amr.d_decl(1,1,1)] # periodicity - geom = amr.Geometry(domain, real_box, coord, is_per); - - # Extract dx from the geometry object - dx = geom.data().CellSize() - - # Nghost = number of ghost cells for each array - Nghost = 1 - - # Ncomp = number of components for each array - Ncomp = 1 +def postprocess_function(state_new, geom): + # Get the real center of the domain from geometry + prob_lo = geom.data().ProbLo() + prob_hi = geom.data().ProbHi() - # How Boxes are distrubuted among MPI processes - dm = amr.DistributionMapping(ba) + # Calculate the actual center coordinates + center_x = 0.5 * (prob_lo[0] + prob_hi[0]) + center_y = 0.5 * (prob_lo[1] + prob_hi[1]) + center_z = 0.5 * (prob_lo[2] + prob_hi[2]) - # Allocate two phi multifabs: one will store the old state, the other the new. - phi_old = amr.MultiFab(ba, dm, Ncomp, Nghost) - phi_new = amr.MultiFab(ba, dm, Ncomp, Nghost) - phi_old.set_val(0.) - phi_new.set_val(0.) - - # time = starting time in the simulation - time = 0. + # Now use these real center coordinates + dx = geom.data().CellSize() - # Ghost cells - ng = phi_old.n_grow_vect - ngx = ng[0] - ngy = ng[1] - ngz = ng[2] + # Convert to index coordinates + i_center = int((center_x - prob_lo[0]) / dx[0]) + j_center = int((center_y - prob_lo[1]) / dx[1]) + k_center = int((center_z - prob_lo[2]) / dx[2]) - # Initialize with parameterized initial condition - for mfi in phi_old: - bx = mfi.validbox() - # phiOld is indexed in reversed order (z,y,x) and indices are local - phiOld = xp.array(phi_old.array(mfi), copy=False) - - x = (xp.arange(bx.small_end[0], bx.big_end[0]+1, 1) + 0.5) * dx[0] - y = (xp.arange(bx.small_end[1], bx.big_end[1]+1, 1) + 0.5) * dx[1] - z = (xp.arange(bx.small_end[2], bx.big_end[2]+1, 1) + 0.5) * dx[2] - - rsquared = ((z[:, xp.newaxis, xp.newaxis] - 0.5)**2 - + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 - + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / init_width - phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + init_amplitude * xp.exp(-rsquared) - - # Write a plotfile of the initial data if plot_int > 0 - if plot_int > 0 and plot_files_output: - step = 0 - pltfile = amr.concatenate("plt", step, 5) - varnames = amr.Vector_string(['phi']) - amr.write_single_level_plotfile(pltfile, phi_old, varnames, geom, time, 0) - - # Time evolution - for step in range(1, nsteps+1): - phi_old.fill_boundary(geom.periodicity()) - - # new_phi = old_phi + dt * Laplacian(old_phi) - # Loop over boxes - for mfi in phi_old: - phiOld = xp.array(phi_old.array(mfi), copy=False) - phiNew = xp.array(phi_new.array(mfi), copy=False) - hix = phiOld.shape[3] - hiy = phiOld.shape[2] - hiz = phiOld.shape[1] - - # Heat equation with parameterized diffusion - # Advance the data by dt - phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( - phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + dt * diffusion_coeff * - (( phiOld[:, ngz :-ngz , ngy :-ngy , ngx+1:hix-ngx+1] - -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] - +phiOld[:, ngz :-ngz , ngy :-ngy , ngx-1:hix-ngx-1]) / dx[0]**2 - +( phiOld[:, ngz :-ngz , ngy+1:hiy-ngy+1, ngx :-ngx ] - -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] - +phiOld[:, ngz :-ngz , ngy-1:hiy-ngy-1, ngx :-ngx ]) / dx[1]**2 - +( phiOld[:, ngz+1:hiz-ngz+1, ngy :-ngy , ngx :-ngx ] - -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] - +phiOld[:, ngz-1:hiz-ngz-1, ngy :-ngy , ngx :-ngx ]) / dx[2]**2)) - - # Update time - time = time + dt - - # Copy new solution into old solution - amr.copy_mfab(dst=phi_old, src=phi_new, srccomp=0, dstcomp=0, numcomp=1, nghost=0) - # Tell the I/O Processor to write out which step we're doing - # amr.Print(f'Advanced step {step}\n') - - # Write a plotfile of the current data (plot_int was defined in the inputs file) - if plot_int > 0 and step%plot_int == 0 and plot_files_output: - pltfile = amr.concatenate("plt", step, 5) - varnames = amr.Vector_string(['phi']) - amr.write_single_level_plotfile(pltfile, phi_new, varnames, geom, time, step) - - # Compute output metrics from final state - - # Find center value at (0.5, 0.5, 0.5) - center_x, center_y, center_z = 0.5, 0.5, 0.5 - - # Convert physical coordinates to global cell indices - i_center = int(center_x / dx[0] - 0.5) - j_center = int(center_y / dx[1] - 0.5) - k_center = int(center_z / dx[2] - 0.5) + center_iv = amr.IntVect3D(i_center, j_center, k_center) center_val = None - for mfi in phi_new: + for mfi in state_new: bx = mfi.validbox() - - # Create IntVect3D for the center point - center_iv = amr.IntVect3D(i_center, j_center, k_center) - - # Check if this box contains the center point if bx.contains(center_iv): - phi_arr = xp.array(phi_new.array(mfi), copy=False) - - # Convert global indices to local array indices + state_arr = xp.array(state_new.array(mfi), copy=False) local_i = i_center - bx.small_end[0] + ngx local_j = j_center - bx.small_end[1] + ngy local_k = k_center - bx.small_end[2] + ngz - - # Extract center value (array indexed as [z,y,x]) - center_val = float(phi_arr[0, local_k, local_j, local_i]) + center_val = float(state_arr[0, local_k, local_j, local_i]) if xp.__name__ == 'cupy': center_val = float(center_val) break - if center_val is None: - center_val = 0.0 + if center_val is None: + center_val = 0.0d # Compute output metrics from final state using PyAMReX built-ins - max_val = phi_new.max(comp=0, local=False) - sum_val = phi_new.sum(comp=0, local=False) + max_val = state_new.max(comp=0, local=False) + sum_val = state_new.sum(comp=0, local=False) # Get total number of valid cells (excluding ghost zones) - total_cells = phi_new.box_array().numPts + total_cells = state_new.box_array().numPts mean_val = sum_val / total_cells # Use L2 norm for standard deviation calculation - l2_norm = phi_new.norm2(0) + l2_norm = state_new.norm2(0) sum_sq = l2_norm**2 variance = (sum_sq / total_cells) - mean_val**2 std_val = np.sqrt(max(0, variance)) @@ -214,97 +65,26 @@ def heat_equation_run(diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01, center_val ]) - -def parse_inputs(): - """Parse inputs using AMReX ParmParse interface.""" - pp = amr.ParmParse("") - - # Add inputs file if it exists - import os - inputs_file = "inputs" - if os.path.exists(inputs_file): - pp.addfile(inputs_file) - - # Read simulation parameters with defaults - n_cell = 32 - pp.query("n_cell", n_cell) - - max_grid_size = 16 - pp.query("max_grid_size", max_grid_size) - - nsteps = 1000 - pp.query("nsteps", nsteps) - - plot_int = 100 - pp.query("plot_int", plot_int) - - dt = 1.0e-5 - pp.query("dt", dt) - - # Read heat equation model parameters with defaults - diffusion_coeff = 1.0 - pp.query("diffusion_coeff", diffusion_coeff) - - init_amplitude = 1.0 - pp.query("init_amplitude", init_amplitude) - - init_width = 0.01 - pp.query("init_width", init_width) - - return { - 'n_cell': n_cell, - 'max_grid_size': max_grid_size, - 'nsteps': nsteps, - 'plot_int': plot_int, - 'dt': dt, - 'diffusion_coeff': diffusion_coeff, - 'init_amplitude': init_amplitude, - 'init_width': init_width - } - -class HeatEquationModel: - """Simple wrapper to make heat equation callable with parameter arrays.""" +class SimulationModel: + """Simple wrapper to make pyamrex simulations callable with parameter arrays.""" def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, plot_int=100, dt=1e-5, use_parmparse=False): - if use_parmparse: - # Conditionally initialize AMReX first if using ParmParse - if not amr.initialized(): - amr.initialize([]) - - # Parse inputs from file - params = parse_inputs() - self.n_cell = params['n_cell'] - self.max_grid_size = params['max_grid_size'] - self.nsteps = params['nsteps'] - self.plot_int = params['plot_int'] - self.dt = params['dt'] - else: - self.n_cell = n_cell - self.max_grid_size = max_grid_size - self.nsteps = nsteps - self.plot_int = plot_int - self.dt = dt - - # Conditionally initialize AMReX - if not amr.initialized(): - amr.initialize([]) + # Conditionally initialize AMReX + if not amr.initialized(): + amr.initialize([]) def __call__(self, params): """ - Run heat equation for each parameter set. + Run simulation for each parameter set. Parameters: ----------- - params : numpy.ndarray of shape (n_samples, 3) - params[:, 0] = diffusion coefficient - params[:, 1] = initial condition amplitude - params[:, 2] = initial condition width + params : numpy.ndarray of shape (n_samples, n_params) (Use get_pnames() to get these names programmatically) Returns: -------- - numpy.ndarray of shape (n_samples, 5) - [max, mean, std, integral, center] for each sample + numpy.ndarray of shape (n_samples, n_outputs) (Use get_outnames() to get these names programmatically) """ if params.ndim == 1: @@ -314,15 +94,8 @@ def __call__(self, params): outputs = np.zeros((n_samples, 5)) for i in range(n_samples): - outputs[i, :] = heat_equation_run( - diffusion_coeff=params[i, 0], - init_amplitude=params[i, 1], - init_width=params[i, 2], - n_cell=self.n_cell, - max_grid_size=self.max_grid_size, - nsteps=self.nsteps, - plot_int=self.plot_int, - dt=self.dt + result = step() + outputs[i, :] = postprocess_function(results) ) return outputs @@ -335,7 +108,7 @@ def get_pnames(self): -------- list : Parameter names corresponding to the input dimensions """ - return ["diffusion coefficient", "initial condition amplitude", "initial condition width"] + return [] def get_outnames(self): """ @@ -345,7 +118,7 @@ def get_outnames(self): -------- list : Output names corresponding to the computed quantities """ - return ["max", "mean", "std", "integral", "center"] + return [] if __name__ == "__main__": diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py new file mode 100755 index 00000000..eecade8c --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright 2023 The AMReX Community +# +# This file is part of AMReX. +# +# License: BSD-3-Clause-LBNL +# Authors: Revathi Jambunathan, Edoardo Zoni, Olga Shapoval, David Grote, Axel Huebl + +import amrex.space3d as amr +import numpy as np + + +def load_cupy(): + if amr.Config.have_gpu: + try: + import cupy as cp + xp = cp + amr.Print("Note: found and will use cupy") + except ImportError: + amr.Print("Warning: GPU found but cupy not available! Trying managed memory in numpy...") + import numpy as np + xp = np + if amr.Config.gpu_backend == "SYCL": + amr.Print("Warning: SYCL GPU backend not yet implemented for Python") + import numpy as np + xp = np + + else: + import numpy as np + xp = np + amr.Print("Note: found and will use numpy") + return xp + + +def main(n_cell=32, max_grid_size=16, nsteps=100, plot_int=100, dt=1e-5, + plot_files_output=False, verbose=1, postprocess_function, + diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01): + """ + The main function, automatically called below if called as a script. + """ + plot_files_output = False + # CPU/GPU logic + xp = load_cupy() + + # AMREX_D_DECL means "do the first X of these, where X is the dimensionality of the simulation" + dom_lo = amr.IntVect(*amr.d_decl( 0, 0, 0)) + dom_hi = amr.IntVect(*amr.d_decl(n_cell-1, n_cell-1, n_cell-1)) + + # Make a single box that is the entire domain + domain = amr.Box(dom_lo, dom_hi) + + # Make BoxArray and Geometry: + # ba contains a list of boxes that cover the domain, + # geom contains information such as the physical domain size, + # number of points in the domain, and periodicity + + # Initialize the boxarray "ba" from the single box "domain" + ba = amr.BoxArray(domain) + # Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction + ba.max_size(max_grid_size) + + # This defines the physical box, [0,1] in each direction. + real_box = amr.RealBox([*amr.d_decl( 0., 0., 0.)], [*amr.d_decl( 1., 1., 1.)]) + + # This defines a Geometry object + # periodic in all direction + coord = 0 # Cartesian + is_per = [*amr.d_decl(1,1,1)] # periodicity + geom = amr.Geometry(domain, real_box, coord, is_per); + + # Extract dx from the geometry object + dx = geom.data().CellSize() + + # Nghost = number of ghost cells for each array + Nghost = 1 + + # Ncomp = number of components for each array + Ncomp = 1 + + # How Boxes are distrubuted among MPI processes + dm = amr.DistributionMapping(ba) + + # Allocate two phi multifabs: one will store the old state, the other the new. + phi_old = amr.MultiFab(ba, dm, Ncomp, Nghost) + phi_new = amr.MultiFab(ba, dm, Ncomp, Nghost) + phi_old.set_val(0.) + phi_new.set_val(0.) + + # time = starting time in the simulation + time = 0. + + # Ghost cells + ng = phi_old.n_grow_vect + ngx = ng[0] + ngy = ng[1] + ngz = ng[2] + + # Loop over boxes + for mfi in phi_old: + bx = mfi.validbox() + # phiOld is indexed in reversed order (z,y,x) and indices are local + phiOld = xp.array(phi_old.array(mfi), copy=False) + # set phi = 1 + amplitude * e^(-(r-0.5)^2 / width) + x = (xp.arange(bx.small_end[0],bx.big_end[0]+1,1) + 0.5) * dx[0] + y = (xp.arange(bx.small_end[1],bx.big_end[1]+1,1) + 0.5) * dx[1] + z = (xp.arange(bx.small_end[2],bx.big_end[2]+1,1) + 0.5) * dx[2] + rsquared = ((z[: , xp.newaxis, xp.newaxis] - 0.5)**2 + + (y[xp.newaxis, : , xp.newaxis] - 0.5)**2 + + (x[xp.newaxis, xp.newaxis, : ] - 0.5)**2) / init_width + phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + init_amplitude * xp.exp(-rsquared) + + # Write a plotfile of the initial data if plot_int > 0 and plot_files_output + if plot_int > 0 and plot_files_output: + step = 0 + pltfile = amr.concatenate("plt", step, 5) + varnames = amr.Vector_string(['phi']) + amr.write_single_level_plotfile(pltfile, phi_old, varnames, geom, time, 0) + + for step in range(1, nsteps+1): + # Fill periodic ghost cells + phi_old.fill_boundary(geom.periodicity()) + + # new_phi = old_phi + dt * Laplacian(old_phi) + # Loop over boxes + for mfi in phi_old: + phiOld = xp.array(phi_old.array(mfi), copy=False) + phiNew = xp.array(phi_new.array(mfi), copy=False) + hix = phiOld.shape[3] + hiy = phiOld.shape[2] + hiz = phiOld.shape[1] + + # Heat equation with parameterized diffusion + # Advance the data by dt + phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( + phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + dt * diffusion_coeff * + (( phiOld[:, ngz :-ngz , ngy :-ngy , ngx+1:hix-ngx+1] + -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] + +phiOld[:, ngz :-ngz , ngy :-ngy , ngx-1:hix-ngx-1]) / dx[0]**2 + +( phiOld[:, ngz :-ngz , ngy+1:hiy-ngy+1, ngx :-ngx ] + -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] + +phiOld[:, ngz :-ngz , ngy-1:hiy-ngy-1, ngx :-ngx ]) / dx[1]**2 + +( phiOld[:, ngz+1:hiz-ngz+1, ngy :-ngy , ngx :-ngx ] + -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] + +phiOld[:, ngz-1:hiz-ngz-1, ngy :-ngy , ngx :-ngx ]) / dx[2]**2)) + + # Update time + time = time + dt + + # Copy new solution into old solution + amr.copy_mfab(dst=phi_old, src=phi_new, srccomp=0, dstcomp=0, numcomp=1, nghost=0) + + # Tell the I/O Processor to write out which step we're doing + if(verbose > 0): + amr.Print(f'Advanced step {step}\n') + + # Write a plotfile of the current data (plot_int was defined in the inputs file) + if plot_int > 0 and step%plot_int == 0 and plot_files_output: + pltfile = amr.concatenate("plt", step, 5) + varnames = amr.Vector_string(['phi']) + amr.write_single_level_plotfile(pltfile, phi_new, varnames, geom, time, step) + +def parse_inputs(): + """Parse inputs using AMReX ParmParse interface.""" + pp = amr.ParmParse("") + + # Add inputs file if it exists + import os + inputs_file = "inputs" + if os.path.exists(inputs_file): + pp.addfile(inputs_file) + + # Read simulation parameters with defaults + n_cell = 32 + pp.query("n_cell", n_cell) + + max_grid_size = 16 + pp.query("max_grid_size", max_grid_size) + + nsteps = 1000 + pp.query("nsteps", nsteps) + + plot_int = 100 + pp.query("plot_int", plot_int) + + dt = 1.0e-5 + pp.query("dt", dt) + + # Read heat equation model parameters with defaults + diffusion_coeff = 1.0 + pp.query("diffusion_coeff", diffusion_coeff) + + init_amplitude = 1.0 + pp.query("init_amplitude", init_amplitude) + + init_width = 0.01 + pp.query("init_width", init_width) + + return { + 'n_cell': n_cell, + 'max_grid_size': max_grid_size, + 'nsteps': nsteps, + 'plot_int': plot_int, + 'dt': dt, + 'diffusion_coeff': diffusion_coeff, + 'init_amplitude': init_amplitude, + 'init_width': init_width + } + +if __name__ == '__main__': + # Initialize AMReX + amr.initialize([]) + + # TODO Implement parser + params = parse_inputs() + # Simulation parameters + # number of cells on each side of the domain + n_cell = 32 + n_cell = params['n_cell'] + # size of each box (or grid) + max_grid_size = 16 + max_grid_size = params['max_grid_size'] + # total steps in simulation + nsteps = 1000 + nsteps = params['nsteps'] + # how often to write a plotfile + plot_int = 100 + plot_int = params['plot_int'] + # time step + dt = 1e-5 + dt = params['dt'] + + main(n_cell, max_grid_size, nsteps, plot_int, dt) + + # Finalize AMReX + amr.finalize() From 6d69b04cef817ad714463116d7033ce0e5be542b Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 08:46:27 -0700 Subject: [PATCH 025/142] Shape of idea --- .../Case-2/HeatEquationModel.py | 356 ++++++++++++------ .../HeatEquation_PythonDriver/Case-2/main.py | 119 +++--- 2 files changed, 291 insertions(+), 184 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py index 53cc8d26..f521d17b 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py @@ -1,156 +1,282 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import amrex.space3d as amr +from abc import ABC, abstractmethod import numpy as np +from typing import Tuple, List, Optional, Union +import amrex.space3d as amr -def postprocess_function(state_new, geom): - # Get the real center of the domain from geometry - prob_lo = geom.data().ProbLo() - prob_hi = geom.data().ProbHi() - - # Calculate the actual center coordinates - center_x = 0.5 * (prob_lo[0] + prob_hi[0]) - center_y = 0.5 * (prob_lo[1] + prob_hi[1]) - center_z = 0.5 * (prob_lo[2] + prob_hi[2]) - - # Now use these real center coordinates - dx = geom.data().CellSize() - - # Convert to index coordinates - i_center = int((center_x - prob_lo[0]) / dx[0]) - j_center = int((center_y - prob_lo[1]) / dx[1]) - k_center = int((center_z - prob_lo[2]) / dx[2]) - - center_iv = amr.IntVect3D(i_center, j_center, k_center) - - center_val = None - for mfi in state_new: - bx = mfi.validbox() - if bx.contains(center_iv): - state_arr = xp.array(state_new.array(mfi), copy=False) - local_i = i_center - bx.small_end[0] + ngx - local_j = j_center - bx.small_end[1] + ngy - local_k = k_center - bx.small_end[2] + ngz - center_val = float(state_arr[0, local_k, local_j, local_i]) - if xp.__name__ == 'cupy': - center_val = float(center_val) - break - - if center_val is None: - center_val = 0.0d - - # Compute output metrics from final state using PyAMReX built-ins - max_val = state_new.max(comp=0, local=False) - sum_val = state_new.sum(comp=0, local=False) - - # Get total number of valid cells (excluding ghost zones) - total_cells = state_new.box_array().numPts +def load_cupy(): + """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" + if amr.Config.have_gpu: + try: + import cupy as cp + amr.Print("Note: found and will use cupy") + return cp + except ImportError: + amr.Print("Warning: GPU found but cupy not available! Using numpy...") + import numpy as np + return np + if amr.Config.gpu_backend == "SYCL": + amr.Print("Warning: SYCL GPU backend not yet implemented for Python") + import numpy as np + return np + else: + import numpy as np + amr.Print("Note: found and will use numpy") + return np + + +def postprocess_function(multifab: amr.MultiFab, geom: amr.Geometry) -> np.ndarray: + """ + Geometry-aware postprocessing function. + + Parameters: + ----------- + multifab : amr.MultiFab + Simulation data + geom : amr.Geometry + Domain geometry + + Returns: + -------- + np.ndarray + Processed outputs [max, mean, std, integral/sum, center_value] + """ + xp = load_cupy() + + # Get basic statistics + max_val = multifab.max(comp=0, local=False) + sum_val = multifab.sum(comp=0, local=False) + total_cells = multifab.box_array().numPts mean_val = sum_val / total_cells - - # Use L2 norm for standard deviation calculation - l2_norm = state_new.norm2(0) + + # Calculate standard deviation + l2_norm = multifab.norm2(0) sum_sq = l2_norm**2 variance = (sum_sq / total_cells) - mean_val**2 std_val = np.sqrt(max(0, variance)) + + # Get value at center + dx = geom.data().CellSize() + prob_lo = geom.data().ProbLo() + prob_hi = geom.data().ProbHi() + + # Calculate center coordinates + center_coords = [(prob_lo[i] + prob_hi[i]) / 2.0 for i in range(geom.Dim())] + + # Find the cell index closest to center + center_indices = [] + for i in range(geom.Dim()): + idx = int((center_coords[i] - prob_lo[i]) / dx[i]) + center_indices.append(idx) + + # Get value at center (default to 0 if can't access) + center_val = 0.0 + try: + for mfi in multifab: + bx = mfi.validbox() + if (center_indices[0] >= bx.small_end[0] and center_indices[0] <= bx.big_end[0] and + center_indices[1] >= bx.small_end[1] and center_indices[1] <= bx.big_end[1] and + center_indices[2] >= bx.small_end[2] and center_indices[2] <= bx.big_end[2]): + + state_arr = xp.array(multifab.array(mfi), copy=False) + # Convert global to local indices + local_i = center_indices[0] - bx.small_end[0] + local_j = center_indices[1] - bx.small_end[1] + local_k = center_indices[2] - bx.small_end[2] + center_val = float(state_arr[0, local_k, local_j, local_i]) + break + except (IndexError, AttributeError): + # Fall back to (0,0,0) if center calculation fails + for mfi in multifab: + state_arr = xp.array(multifab.array(mfi), copy=False) + center_val = float(state_arr[0, 0, 0, 0]) + break + + return np.array([max_val, mean_val, std_val, sum_val, center_val]) - integral = sum_val * dx[0] * dx[1] * dx[2] - - return np.array([ - max_val, - mean_val, - std_val, - integral, - center_val - ]) -class SimulationModel: - """Simple wrapper to make pyamrex simulations callable with parameter arrays.""" +class SimulationModel(ABC): + """Base class defining the simulation interface.""" - def __init__(self, n_cell=32, max_grid_size=16, nsteps=1000, plot_int=100, dt=1e-5, use_parmparse=False): - # Conditionally initialize AMReX + def __init__(self, **kwargs): + """Initialize AMReX if needed.""" if not amr.initialized(): amr.initialize([]) - def __call__(self, params): + def __call__(self, params: np.ndarray) -> np.ndarray: """ Run simulation for each parameter set. Parameters: ----------- - params : numpy.ndarray of shape (n_samples, n_params) - (Use get_pnames() to get these names programmatically) + params : np.ndarray of shape (n_samples, n_params) or (n_params,) + Parameter sets to run Returns: -------- - numpy.ndarray of shape (n_samples, n_outputs) - (Use get_outnames() to get these names programmatically) + np.ndarray of shape (n_samples, n_outputs) + Simulation outputs """ if params.ndim == 1: params = params.reshape(1, -1) n_samples = params.shape[0] - outputs = np.zeros((n_samples, 5)) + n_outputs = len(self.get_outnames()) + outputs = np.zeros((n_samples, n_outputs)) for i in range(n_samples): - result = step() - outputs[i, :] = postprocess_function(results) - ) + try: + multifab, varnames, geom = self.evolve(params[i, :]) + outputs[i, :] = self.postprocess(multifab, varnames, geom) + except Exception as e: + amr.Print(f"Warning: Simulation failed for parameter set {i}: {e}") + outputs[i, :] = np.nan return outputs - def get_pnames(self): + @abstractmethod + def evolve(self, param_set: np.ndarray) -> Tuple[amr.MultiFab, Optional[amr.Vector_string], Optional[amr.Geometry]]: """ - Get parameter names for the heat equation model. - + Run a single simulation step/evolution. + + Parameters: + ----------- + param_set : np.ndarray + Single parameter set + Returns: -------- - list : Parameter names corresponding to the input dimensions + tuple : (multifab, varnames, geom) + - multifab: simulation data + - varnames: variable names or None + - geom: domain geometry or None """ - return [] + pass - def get_outnames(self): + def postprocess(self, multifab: amr.MultiFab, varnames: Optional[amr.Vector_string], + geom: Optional[amr.Geometry]) -> np.ndarray: """ - Get output names for the heat equation model. - + Postprocessing function with geometry awareness. + + Parameters: + ----------- + multifab : amr.MultiFab + Simulation data + varnames : amr.Vector_string or None + Variable names + geom : amr.Geometry or None + Domain geometry + Returns: -------- - list : Output names corresponding to the computed quantities + np.ndarray + Processed outputs [max, mean, std, integral/sum, center_value] """ - return [] - -if __name__ == "__main__": - - # Initialize AMReX - amr.initialize([]) - - # Create model using ParmParse to read from inputs file - model = HeatEquationModel(use_parmparse=True) - - print(f"Heat equation model initialized with:") - print(f" n_cell = {model.n_cell}") - print(f" max_grid_size = {model.max_grid_size}") - print(f" nsteps = {model.nsteps}") - print(f" plot_int = {model.plot_int}") - print(f" dt = {model.dt}") - - # Test with random parameters - test_params = np.array([ - [1.0, 1.0, 0.01], # baseline - [2.0, 1.5, 0.02], # higher diffusion, higher amplitude - [0.5, 2.0, 0.005] # lower diffusion, higher amplitude, narrower - ]) - - print("\nRunning heat equation with parameters:") - print(" [diffusion, amplitude, width]") - print(test_params) - - outputs = model(test_params) - - print("\nResults [max, mean, std, integral, center]:") - print(outputs) - - # Finalize AMReX - amr.finalize() + if geom is not None: + return postprocess_function(multifab, geom) + else: + return self._postprocess_no_geom(multifab) + def _postprocess_no_geom(self, multifab: amr.MultiFab) -> np.ndarray: + """ + Postprocessing when geometry is not available. + Uses (0,0,0,0) cell instead of center. + """ + xp = load_cupy() + + # Get value at (0,0,0,0) cell + cell_val = 0.0 + try: + for mfi in multifab: + state_arr = xp.array(multifab.array(mfi), copy=False) + cell_val = float(state_arr[0, 0, 0, 0]) # component 0, cell (0,0,0) + break + except (IndexError, AttributeError): + amr.Print("Warning: Could not access cell (0,0,0,0)") + + # Compute statistics + max_val = multifab.max(comp=0, local=False) + sum_val = multifab.sum(comp=0, local=False) + total_cells = multifab.box_array().numPts + mean_val = sum_val / total_cells + + l2_norm = multifab.norm2(0) + sum_sq = l2_norm**2 + variance = (sum_sq / total_cells) - mean_val**2 + std_val = np.sqrt(max(0, variance)) + + return np.array([max_val, mean_val, std_val, sum_val, cell_val]) + + @abstractmethod + def get_pnames(self) -> List[str]: + """Get parameter names.""" + pass + + @abstractmethod + def get_outnames(self) -> List[str]: + """Get output names.""" + pass + + +class HeatEquationModel(SimulationModel): + """Heat equation simulation model.""" + + def __init__(self, n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 1000, + plot_int: int = 100, dt: float = 1e-5, use_parmparse: bool = False): + super().__init__() + + if use_parmparse: + from main import parse_inputs # Import here to avoid circular imports + params = parse_inputs() + self.n_cell = params['n_cell'] + self.max_grid_size = params['max_grid_size'] + self.nsteps = params['nsteps'] + self.plot_int = params['plot_int'] + self.dt = params['dt'] + else: + self.n_cell = n_cell + self.max_grid_size = max_grid_size + self.nsteps = nsteps + self.plot_int = plot_int + self.dt = dt + + def evolve(self, param_set: np.ndarray) -> Tuple[amr.MultiFab, amr.Vector_string, amr.Geometry]: + """ + Run heat equation simulation. + + Parameters: + ----------- + param_set : np.ndarray + [diffusion_coeff, init_amplitude, init_width] + + Returns: + -------- + tuple : (phi_new, varnames, geom) + Ready to pass to write_single_level_plotfile + """ + from main import main # Import here to avoid circular imports + + if len(param_set) != 3: + raise ValueError(f"Expected 3 parameters, got {len(param_set)}") + + phi_new, geom = main( + diffusion_coeff=float(param_set[0]), + init_amplitude=float(param_set[1]), + init_width=float(param_set[2]), + n_cell=self.n_cell, + max_grid_size=self.max_grid_size, + nsteps=self.nsteps, + plot_int=self.plot_int, + dt=self.dt, + plot_files_output=False, + verbose=0 + ) + + varnames = amr.Vector_string(['phi']) + return phi_new, varnames, geom + + def get_pnames(self) -> List[str]: + return ["diffusion_coefficient", "initial_amplitude", "initial_width"] + + def get_outnames(self) -> List[str]: + return ["max", "mean", "std", "integral", "center"] diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py index eecade8c..f0323523 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py @@ -10,37 +10,43 @@ import amrex.space3d as amr import numpy as np +from typing import Tuple, Dict, Union def load_cupy(): + """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" if amr.Config.have_gpu: try: import cupy as cp - xp = cp amr.Print("Note: found and will use cupy") + return cp except ImportError: amr.Print("Warning: GPU found but cupy not available! Trying managed memory in numpy...") import numpy as np - xp = np + return np if amr.Config.gpu_backend == "SYCL": amr.Print("Warning: SYCL GPU backend not yet implemented for Python") import numpy as np - xp = np - + return np else: import numpy as np - xp = np amr.Print("Note: found and will use numpy") - return xp + return np -def main(n_cell=32, max_grid_size=16, nsteps=100, plot_int=100, dt=1e-5, - plot_files_output=False, verbose=1, postprocess_function, - diffusion_coeff=1.0, init_amplitude=1.0, init_width=0.01): +def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, + plot_int: int = 100, dt: float = 1e-5, plot_files_output: bool = False, + verbose: int = 1, diffusion_coeff: float = 1.0, init_amplitude: float = 1.0, + init_width: float = 0.01) -> Tuple[amr.MultiFab, amr.Geometry]: """ + Run the heat equation simulation. The main function, automatically called below if called as a script. + + Returns: + -------- + tuple : (phi_new, geom) + Final state and geometry """ - plot_files_output = False # CPU/GPU logic xp = load_cupy() @@ -161,7 +167,10 @@ def main(n_cell=32, max_grid_size=16, nsteps=100, plot_int=100, dt=1e-5, varnames = amr.Vector_string(['phi']) amr.write_single_level_plotfile(pltfile, phi_new, varnames, geom, time, step) -def parse_inputs(): + return phi_new, geom + + +def parse_inputs() -> Dict[str, Union[int, float]]: """Parse inputs using AMReX ParmParse interface.""" pp = amr.ParmParse("") @@ -171,67 +180,39 @@ def parse_inputs(): if os.path.exists(inputs_file): pp.addfile(inputs_file) - # Read simulation parameters with defaults - n_cell = 32 - pp.query("n_cell", n_cell) - - max_grid_size = 16 - pp.query("max_grid_size", max_grid_size) - - nsteps = 1000 - pp.query("nsteps", nsteps) - - plot_int = 100 - pp.query("plot_int", plot_int) - - dt = 1.0e-5 - pp.query("dt", dt) - - # Read heat equation model parameters with defaults - diffusion_coeff = 1.0 - pp.query("diffusion_coeff", diffusion_coeff) - - init_amplitude = 1.0 - pp.query("init_amplitude", init_amplitude) - - init_width = 0.01 - pp.query("init_width", init_width) - - return { - 'n_cell': n_cell, - 'max_grid_size': max_grid_size, - 'nsteps': nsteps, - 'plot_int': plot_int, - 'dt': dt, - 'diffusion_coeff': diffusion_coeff, - 'init_amplitude': init_amplitude, - 'init_width': init_width + # Default values + defaults = { + 'n_cell': 32, + 'max_grid_size': 16, + 'nsteps': 1000, + 'plot_int': 100, + 'dt': 1.0e-5, + 'diffusion_coeff': 1.0, + 'init_amplitude': 1.0, + 'init_width': 0.01 } + + # Parse parameters + params = {} + for key, default_value in defaults.items(): + value = default_value + pp.query(key, value) + params[key] = value + + return params + if __name__ == '__main__': # Initialize AMReX amr.initialize([]) - # TODO Implement parser - params = parse_inputs() - # Simulation parameters - # number of cells on each side of the domain - n_cell = 32 - n_cell = params['n_cell'] - # size of each box (or grid) - max_grid_size = 16 - max_grid_size = params['max_grid_size'] - # total steps in simulation - nsteps = 1000 - nsteps = params['nsteps'] - # how often to write a plotfile - plot_int = 100 - plot_int = params['plot_int'] - # time step - dt = 1e-5 - dt = params['dt'] - - main(n_cell, max_grid_size, nsteps, plot_int, dt) - - # Finalize AMReX - amr.finalize() + try: + # Parse inputs + params = parse_inputs() + + # Run simulation + main(**params) + + finally: + # Finalize AMReX + amr.finalize() From b1f2c2d85513742d5cd0af995c756abf80caff55 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 09:46:51 -0700 Subject: [PATCH 026/142] Main works again and uses inputs file flags if they're there --- .../HeatEquation_PythonDriver/Case-2/main.py | 59 +++++++++++++++---- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py index f0323523..a6b044b3 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py @@ -170,8 +170,8 @@ def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, return phi_new, geom -def parse_inputs() -> Dict[str, Union[int, float]]: - """Parse inputs using AMReX ParmParse interface.""" +def parse_inputs() -> Dict[str, Union[int, float, bool]]: + """Parse inputs using AMReX ParmParse to_dict method.""" pp = amr.ParmParse("") # Add inputs file if it exists @@ -180,27 +180,62 @@ def parse_inputs() -> Dict[str, Union[int, float]]: if os.path.exists(inputs_file): pp.addfile(inputs_file) - # Default values + # Default values with their types defaults = { 'n_cell': 32, 'max_grid_size': 16, 'nsteps': 1000, 'plot_int': 100, 'dt': 1.0e-5, + 'plot_files_output': False, 'diffusion_coeff': 1.0, 'init_amplitude': 1.0, 'init_width': 0.01 } - # Parse parameters - params = {} - for key, default_value in defaults.items(): - value = default_value - pp.query(key, value) - params[key] = value - - return params - + try: + # Convert entire ParmParse table to Python dictionary + all_params = pp.to_dict() + + # Extract our specific parameters with proper type conversion + params = {} + for key, default_value in defaults.items(): + if key in all_params: + try: + # Convert string to appropriate type based on default + if isinstance(default_value, int): + params[key] = int(all_params[key]) + elif isinstance(default_value, float): + params[key] = float(all_params[key]) + elif isinstance(default_value, bool): + # Handle boolean conversion from string + val_str = str(all_params[key]).lower() + if val_str in ('true', '1', 'yes', 'on'): + params[key] = True + elif val_str in ('false', '0', 'no', 'off'): + params[key] = False + else: + # If unrecognized, use default + params[key] = default_value + else: + params[key] = all_params[key] + except (ValueError, TypeError) as e: + amr.Print(f"Warning: Could not convert parameter {key}='{all_params[key]}' to {type(default_value).__name__}, using default: {default_value}") + params[key] = default_value + else: + params[key] = default_value + + # Optional: print the parameters we're actually using + amr.Print("Using parameters:") + for key, value in params.items(): + amr.Print(f" {key}: {value}") + + return params + + except Exception as e: + amr.Print(f"Warning: Could not parse parameters with to_dict(): {e}") + amr.Print("Using default values") + return defaults if __name__ == '__main__': # Initialize AMReX From e33bea2e2a08aaa19d8abc69c5922d65a8b44e63 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 09:50:18 -0700 Subject: [PATCH 027/142] Fix shebang --- .../HeatEquation_PythonDriver/Case-2/HeatEquationModel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py index f521d17b..7737a349 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + from abc import ABC, abstractmethod import numpy as np from typing import Tuple, List, Optional, Union From 89ae1608fa0d87ed0d47a59bcc6dac5d640e99d6 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 09:53:53 -0700 Subject: [PATCH 028/142] Make spacedim be 3 by default, add test --- .../Case-2/HeatEquationModel.py | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py index 7737a349..dd0a2bd6 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py @@ -64,11 +64,11 @@ def postprocess_function(multifab: amr.MultiFab, geom: amr.Geometry) -> np.ndarr prob_hi = geom.data().ProbHi() # Calculate center coordinates - center_coords = [(prob_lo[i] + prob_hi[i]) / 2.0 for i in range(geom.Dim())] + center_coords = [(prob_lo[i] + prob_hi[i]) / 2.0 for i in range(3)] # Find the cell index closest to center center_indices = [] - for i in range(geom.Dim()): + for i in range(3): idx = int((center_coords[i] - prob_lo[i]) / dx[i]) center_indices.append(idx) @@ -283,3 +283,37 @@ def get_pnames(self) -> List[str]: def get_outnames(self) -> List[str]: return ["max", "mean", "std", "integral", "center"] + +if __name__ == "__main__": + + # Initialize AMReX + amr.initialize([]) + + # Create model using ParmParse to read from inputs file + model = HeatEquationModel(use_parmparse=True) + + print(f"Heat equation model initialized with:") + print(f" n_cell = {model.n_cell}") + print(f" max_grid_size = {model.max_grid_size}") + print(f" nsteps = {model.nsteps}") + print(f" plot_int = {model.plot_int}") + print(f" dt = {model.dt}") + + # Test with random parameters + test_params = np.array([ + [1.0, 1.0, 0.01], # baseline + [2.0, 1.5, 0.02], # higher diffusion, higher amplitude + [0.5, 2.0, 0.005] # lower diffusion, higher amplitude, narrower + ]) + + print("\nRunning heat equation with parameters:") + print(" [diffusion, amplitude, width]") + print(test_params) + + outputs = model(test_params) + + print("\nResults [max, mean, std, integral, center]:") + print(outputs) + + # Finalize AMReX + amr.finalize() From 476a9affcd8fda09ec5501115897f633073f0f80 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 10:09:47 -0700 Subject: [PATCH 029/142] Split files again --- .../Case-2/BaseModel.py | 119 ++++++ .../Case-2/HeatEquationModel.py | 338 +++++------------- 2 files changed, 204 insertions(+), 253 deletions(-) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py new file mode 100644 index 00000000..6b4dd32a --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py @@ -0,0 +1,119 @@ +from abc import ABC, abstractmethod +import numpy as np +from typing import Tuple, List, Optional, Union +import amrex.space3d as amr + + +def load_cupy(): + """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" + if amr.Config.have_gpu: + try: + import cupy as cp + amr.Print("Note: found and will use cupy") + return cp + except ImportError: + amr.Print("Warning: GPU found but cupy not available! Using numpy...") + import numpy as np + return np + if amr.Config.gpu_backend == "SYCL": + amr.Print("Warning: SYCL GPU backend not yet implemented for Python") + import numpy as np + return np + else: + import numpy as np + amr.Print("Note: found and will use numpy") + return np + + +class SimulationModel(ABC): + """Base class defining the simulation interface.""" + + def __init__(self, **kwargs): + """Initialize AMReX if needed.""" + if not amr.initialized(): + amr.initialize([]) + + def __call__(self, params: np.ndarray) -> np.ndarray: + """ + Run simulation for each parameter set. + + Parameters: + ----------- + params : np.ndarray of shape (n_samples, n_params) or (n_params,) + Parameter sets to run + + Returns: + -------- + np.ndarray of shape (n_samples, n_outputs) + Simulation outputs + """ + if params.ndim == 1: + params = params.reshape(1, -1) + + n_samples = params.shape[0] + n_outputs = len(self.get_outnames()) + outputs = np.zeros((n_samples, n_outputs)) + + for i in range(n_samples): + try: + multifab, varnames, geom = self.evolve(params[i, :]) + outputs[i, :] = self.postprocess(multifab, varnames, geom) + except Exception as e: + amr.Print(f"Warning: Simulation failed for parameter set {i}: {e}") + outputs[i, :] = np.nan + + return outputs + + @abstractmethod + def evolve(self, param_set: np.ndarray) -> Tuple[amr.MultiFab, Optional[amr.Vector_string], Optional[amr.Geometry]]: + """ + Run a single simulation step/evolution. + + Parameters: + ----------- + param_set : np.ndarray + Single parameter set + + Returns: + -------- + tuple : (multifab, varnames, geom) + - multifab: simulation data + - varnames: variable names or None + - geom: domain geometry or None + """ + pass + + def postprocess(self, multifab: amr.MultiFab, varnames: Optional[amr.Vector_string], + geom: Optional[amr.Geometry]) -> np.ndarray: + """ + Basic postprocessing with simple one-line operations. + + Parameters: + ----------- + multifab : amr.MultiFab + Simulation data + varnames : amr.Vector_string or None + Variable names (unused in base implementation) + geom : amr.Geometry or None + Domain geometry (unused in base implementation) + + Returns: + -------- + np.ndarray + Processed outputs [max, sum, l2_norm] + """ + max_val = multifab.max(comp=0, local=False) + sum_val = multifab.sum(comp=0, local=False) + l2_norm = multifab.norm2(0) + + return np.array([max_val, sum_val, l2_norm]) + + @abstractmethod + def get_pnames(self) -> List[str]: + """Get parameter names.""" + pass + + @abstractmethod + def get_outnames(self) -> List[str]: + """Get output names.""" + pass diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py index dd0a2bd6..33b6738a 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py @@ -1,224 +1,8 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -from abc import ABC, abstractmethod +#!/bin/python3 import numpy as np -from typing import Tuple, List, Optional, Union +from typing import Tuple, List, Optional import amrex.space3d as amr - - -def load_cupy(): - """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" - if amr.Config.have_gpu: - try: - import cupy as cp - amr.Print("Note: found and will use cupy") - return cp - except ImportError: - amr.Print("Warning: GPU found but cupy not available! Using numpy...") - import numpy as np - return np - if amr.Config.gpu_backend == "SYCL": - amr.Print("Warning: SYCL GPU backend not yet implemented for Python") - import numpy as np - return np - else: - import numpy as np - amr.Print("Note: found and will use numpy") - return np - - -def postprocess_function(multifab: amr.MultiFab, geom: amr.Geometry) -> np.ndarray: - """ - Geometry-aware postprocessing function. - - Parameters: - ----------- - multifab : amr.MultiFab - Simulation data - geom : amr.Geometry - Domain geometry - - Returns: - -------- - np.ndarray - Processed outputs [max, mean, std, integral/sum, center_value] - """ - xp = load_cupy() - - # Get basic statistics - max_val = multifab.max(comp=0, local=False) - sum_val = multifab.sum(comp=0, local=False) - total_cells = multifab.box_array().numPts - mean_val = sum_val / total_cells - - # Calculate standard deviation - l2_norm = multifab.norm2(0) - sum_sq = l2_norm**2 - variance = (sum_sq / total_cells) - mean_val**2 - std_val = np.sqrt(max(0, variance)) - - # Get value at center - dx = geom.data().CellSize() - prob_lo = geom.data().ProbLo() - prob_hi = geom.data().ProbHi() - - # Calculate center coordinates - center_coords = [(prob_lo[i] + prob_hi[i]) / 2.0 for i in range(3)] - - # Find the cell index closest to center - center_indices = [] - for i in range(3): - idx = int((center_coords[i] - prob_lo[i]) / dx[i]) - center_indices.append(idx) - - # Get value at center (default to 0 if can't access) - center_val = 0.0 - try: - for mfi in multifab: - bx = mfi.validbox() - if (center_indices[0] >= bx.small_end[0] and center_indices[0] <= bx.big_end[0] and - center_indices[1] >= bx.small_end[1] and center_indices[1] <= bx.big_end[1] and - center_indices[2] >= bx.small_end[2] and center_indices[2] <= bx.big_end[2]): - - state_arr = xp.array(multifab.array(mfi), copy=False) - # Convert global to local indices - local_i = center_indices[0] - bx.small_end[0] - local_j = center_indices[1] - bx.small_end[1] - local_k = center_indices[2] - bx.small_end[2] - center_val = float(state_arr[0, local_k, local_j, local_i]) - break - except (IndexError, AttributeError): - # Fall back to (0,0,0) if center calculation fails - for mfi in multifab: - state_arr = xp.array(multifab.array(mfi), copy=False) - center_val = float(state_arr[0, 0, 0, 0]) - break - - return np.array([max_val, mean_val, std_val, sum_val, center_val]) - - -class SimulationModel(ABC): - """Base class defining the simulation interface.""" - - def __init__(self, **kwargs): - """Initialize AMReX if needed.""" - if not amr.initialized(): - amr.initialize([]) - - def __call__(self, params: np.ndarray) -> np.ndarray: - """ - Run simulation for each parameter set. - - Parameters: - ----------- - params : np.ndarray of shape (n_samples, n_params) or (n_params,) - Parameter sets to run - - Returns: - -------- - np.ndarray of shape (n_samples, n_outputs) - Simulation outputs - """ - if params.ndim == 1: - params = params.reshape(1, -1) - - n_samples = params.shape[0] - n_outputs = len(self.get_outnames()) - outputs = np.zeros((n_samples, n_outputs)) - - for i in range(n_samples): - try: - multifab, varnames, geom = self.evolve(params[i, :]) - outputs[i, :] = self.postprocess(multifab, varnames, geom) - except Exception as e: - amr.Print(f"Warning: Simulation failed for parameter set {i}: {e}") - outputs[i, :] = np.nan - - return outputs - - @abstractmethod - def evolve(self, param_set: np.ndarray) -> Tuple[amr.MultiFab, Optional[amr.Vector_string], Optional[amr.Geometry]]: - """ - Run a single simulation step/evolution. - - Parameters: - ----------- - param_set : np.ndarray - Single parameter set - - Returns: - -------- - tuple : (multifab, varnames, geom) - - multifab: simulation data - - varnames: variable names or None - - geom: domain geometry or None - """ - pass - - def postprocess(self, multifab: amr.MultiFab, varnames: Optional[amr.Vector_string], - geom: Optional[amr.Geometry]) -> np.ndarray: - """ - Postprocessing function with geometry awareness. - - Parameters: - ----------- - multifab : amr.MultiFab - Simulation data - varnames : amr.Vector_string or None - Variable names - geom : amr.Geometry or None - Domain geometry - - Returns: - -------- - np.ndarray - Processed outputs [max, mean, std, integral/sum, center_value] - """ - if geom is not None: - return postprocess_function(multifab, geom) - else: - return self._postprocess_no_geom(multifab) - - def _postprocess_no_geom(self, multifab: amr.MultiFab) -> np.ndarray: - """ - Postprocessing when geometry is not available. - Uses (0,0,0,0) cell instead of center. - """ - xp = load_cupy() - - # Get value at (0,0,0,0) cell - cell_val = 0.0 - try: - for mfi in multifab: - state_arr = xp.array(multifab.array(mfi), copy=False) - cell_val = float(state_arr[0, 0, 0, 0]) # component 0, cell (0,0,0) - break - except (IndexError, AttributeError): - amr.Print("Warning: Could not access cell (0,0,0,0)") - - # Compute statistics - max_val = multifab.max(comp=0, local=False) - sum_val = multifab.sum(comp=0, local=False) - total_cells = multifab.box_array().numPts - mean_val = sum_val / total_cells - - l2_norm = multifab.norm2(0) - sum_sq = l2_norm**2 - variance = (sum_sq / total_cells) - mean_val**2 - std_val = np.sqrt(max(0, variance)) - - return np.array([max_val, mean_val, std_val, sum_val, cell_val]) - - @abstractmethod - def get_pnames(self) -> List[str]: - """Get parameter names.""" - pass - - @abstractmethod - def get_outnames(self) -> List[str]: - """Get output names.""" - pass +from BaseModel import SimulationModel, load_cupy class HeatEquationModel(SimulationModel): @@ -278,42 +62,90 @@ def evolve(self, param_set: np.ndarray) -> Tuple[amr.MultiFab, amr.Vector_string varnames = amr.Vector_string(['phi']) return phi_new, varnames, geom + def postprocess(self, multifab: amr.MultiFab, varnames: Optional[amr.Vector_string], + geom: Optional[amr.Geometry]) -> np.ndarray: + """ + Heat equation specific postprocessing with geometry awareness. + + Parameters: + ----------- + multifab : amr.MultiFab + Simulation data + varnames : amr.Vector_string or None + Variable names + geom : amr.Geometry or None + Domain geometry + + Returns: + -------- + np.ndarray + Processed outputs [max, mean, std, integral/sum, center_value] + """ + xp = load_cupy() + + # Get basic statistics + max_val = multifab.max(comp=0, local=False) + sum_val = multifab.sum(comp=0, local=False) + total_cells = multifab.box_array().numPts + mean_val = sum_val / total_cells + + # Calculate standard deviation + l2_norm = multifab.norm2(0) + sum_sq = l2_norm**2 + variance = (sum_sq / total_cells) - mean_val**2 + std_val = np.sqrt(max(0, variance)) + + # Get value at center (if geometry available) + center_val = 0.0 + if geom is not None: + dx = geom.data().CellSize() + prob_lo = geom.data().ProbLo() + prob_hi = geom.data().ProbHi() + + # Calculate center coordinates + center_coords = [(prob_lo[i] + prob_hi[i]) / 2.0 for i in range(3)] + + # Find the cell index closest to center + center_indices = [] + for i in range(3): + idx = int((center_coords[i] - prob_lo[i]) / dx[i]) + center_indices.append(idx) + + # Get value at center (default to 0 if can't access) + try: + for mfi in multifab: + bx = mfi.validbox() + if (center_indices[0] >= bx.small_end[0] and center_indices[0] <= bx.big_end[0] and + center_indices[1] >= bx.small_end[1] and center_indices[1] <= bx.big_end[1] and + center_indices[2] >= bx.small_end[2] and center_indices[2] <= bx.big_end[2]): + + state_arr = xp.array(multifab.array(mfi), copy=False) + # Convert global to local indices + local_i = center_indices[0] - bx.small_end[0] + local_j = center_indices[1] - bx.small_end[1] + local_k = center_indices[2] - bx.small_end[2] + center_val = float(state_arr[0, local_k, local_j, local_i]) + break + except (IndexError, AttributeError): + # Fall back to (0,0,0) if center calculation fails + for mfi in multifab: + state_arr = xp.array(multifab.array(mfi), copy=False) + center_val = float(state_arr[0, 0, 0, 0]) + break + else: + # No geometry available, use (0,0,0,0) cell + try: + for mfi in multifab: + state_arr = xp.array(multifab.array(mfi), copy=False) + center_val = float(state_arr[0, 0, 0, 0]) + break + except (IndexError, AttributeError): + amr.Print("Warning: Could not access cell (0,0,0,0)") + + return np.array([max_val, mean_val, std_val, sum_val, center_val]) + def get_pnames(self) -> List[str]: return ["diffusion_coefficient", "initial_amplitude", "initial_width"] def get_outnames(self) -> List[str]: return ["max", "mean", "std", "integral", "center"] - -if __name__ == "__main__": - - # Initialize AMReX - amr.initialize([]) - - # Create model using ParmParse to read from inputs file - model = HeatEquationModel(use_parmparse=True) - - print(f"Heat equation model initialized with:") - print(f" n_cell = {model.n_cell}") - print(f" max_grid_size = {model.max_grid_size}") - print(f" nsteps = {model.nsteps}") - print(f" plot_int = {model.plot_int}") - print(f" dt = {model.dt}") - - # Test with random parameters - test_params = np.array([ - [1.0, 1.0, 0.01], # baseline - [2.0, 1.5, 0.02], # higher diffusion, higher amplitude - [0.5, 2.0, 0.005] # lower diffusion, higher amplitude, narrower - ]) - - print("\nRunning heat equation with parameters:") - print(" [diffusion, amplitude, width]") - print(test_params) - - outputs = model(test_params) - - print("\nResults [max, mean, std, integral, center]:") - print(outputs) - - # Finalize AMReX - amr.finalize() From 99b5079fa5661827d34191594c39eb70a100f4bb Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 10:13:02 -0700 Subject: [PATCH 030/142] Fix shebang and add test --- .../Case-2/BaseModel.py | 1 + .../Case-2/HeatEquationModel.py | 36 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py index 6b4dd32a..d06ce345 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python3 from abc import ABC, abstractmethod import numpy as np from typing import Tuple, List, Optional, Union diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py index 33b6738a..dd9fc0b5 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py @@ -1,4 +1,4 @@ -#!/bin/python3 +#!/usr/bin/env python3 import numpy as np from typing import Tuple, List, Optional import amrex.space3d as amr @@ -149,3 +149,37 @@ def get_pnames(self) -> List[str]: def get_outnames(self) -> List[str]: return ["max", "mean", "std", "integral", "center"] + +if __name__ == "__main__": + + # Initialize AMReX + amr.initialize([]) + + # Create model using ParmParse to read from inputs file + model = HeatEquationModel(use_parmparse=True) + + print(f"Heat equation model initialized with:") + print(f" n_cell = {model.n_cell}") + print(f" max_grid_size = {model.max_grid_size}") + print(f" nsteps = {model.nsteps}") + print(f" plot_int = {model.plot_int}") + print(f" dt = {model.dt}") + + # Test with random parameters + test_params = np.array([ + [1.0, 1.0, 0.01], # baseline + [2.0, 1.5, 0.02], # higher diffusion, higher amplitude + [0.5, 2.0, 0.005] # lower diffusion, higher amplitude, narrower + ]) + + print("\nRunning heat equation with parameters:") + print(" [diffusion, amplitude, width]") + print(test_params) + + outputs = model(test_params) + + print("\nResults [max, mean, std, integral, center]:") + print(outputs) + + # Finalize AMReX + amr.finalize() From 16a039004f88f2d303eacd3415e016c5e50591e7 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 10:13:18 -0700 Subject: [PATCH 031/142] Use inputs to be shorter --- GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs index 5b098f82..65a1c23d 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs @@ -1,7 +1,11 @@ n_cell = 32 max_grid_size = 16 -nsteps = 1000 +nsteps = 10 plot_int = 100 dt = 1.e-5 + +plot_files_output = False + +verbose = 0 \ No newline at end of file From cc8c9f5dfc457a9e0a3213b0b4c326a24fa8258f Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 10:22:56 -0700 Subject: [PATCH 032/142] Move --- .../HeatEquation_PythonDriver/{Case-1 => Case-3}/CMakeLists.txt | 0 .../HeatEquation_PythonDriver/{Case-1 => Case-3}/GNUmakefile | 0 .../HeatEquation_PythonDriver/{Case-1 => Case-3}/Make.package | 0 .../HeatEquation_PythonDriver/{Case-1 => Case-3}/README.md | 0 .../HeatEquation_PythonDriver/{Case-1 => Case-3}/bindings.cpp | 0 .../HeatEquation_PythonDriver/{Case-1 => Case-3}/inputs | 0 .../HeatEquation_PythonDriver/{Case-1 => Case-3}/main.cpp | 0 .../HeatEquation_PythonDriver/{Case-1 => Case-3}/pybind11.cmake | 0 .../HeatEquation_PythonDriver/{Case-1 => Case-3}/test.py | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-1 => Case-3}/CMakeLists.txt (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-1 => Case-3}/GNUmakefile (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-1 => Case-3}/Make.package (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-1 => Case-3}/README.md (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-1 => Case-3}/bindings.cpp (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-1 => Case-3}/inputs (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-1 => Case-3}/main.cpp (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-1 => Case-3}/pybind11.cmake (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-1 => Case-3}/test.py (100%) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/CMakeLists.txt b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/CMakeLists.txt similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/CMakeLists.txt rename to GuidedTutorials/HeatEquation_PythonDriver/Case-3/CMakeLists.txt diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/GNUmakefile similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile rename to GuidedTutorials/HeatEquation_PythonDriver/Case-3/GNUmakefile diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/Make.package similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package rename to GuidedTutorials/HeatEquation_PythonDriver/Case-3/Make.package diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/README.md b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/README.md similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/README.md rename to GuidedTutorials/HeatEquation_PythonDriver/Case-3/README.md diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/bindings.cpp similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.cpp rename to GuidedTutorials/HeatEquation_PythonDriver/Case-3/bindings.cpp diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/inputs similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs rename to GuidedTutorials/HeatEquation_PythonDriver/Case-3/inputs diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/main.cpp similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp rename to GuidedTutorials/HeatEquation_PythonDriver/Case-3/main.cpp diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/pybind11.cmake b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/pybind11.cmake similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/pybind11.cmake rename to GuidedTutorials/HeatEquation_PythonDriver/Case-3/pybind11.cmake diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/test.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/test.py similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/test.py rename to GuidedTutorials/HeatEquation_PythonDriver/Case-3/test.py From 396fac30072b8a93cfb3beec5c90d219d8619da8 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 10:23:29 -0700 Subject: [PATCH 033/142] Initial copy from HeatEquation_Simple --- .../Case-1/CMakeLists.txt | 72 ++++++ .../Case-1/GNUmakefile | 16 ++ .../Case-1/Make.package | 2 + .../Case-1/Visualization.ipynb | 200 ++++++++++++++++ .../HeatEquation_PythonDriver/Case-1/inputs | 7 + .../HeatEquation_PythonDriver/Case-1/main.cpp | 226 ++++++++++++++++++ 6 files changed, 523 insertions(+) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/CMakeLists.txt create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/Visualization.ipynb create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/CMakeLists.txt b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/CMakeLists.txt new file mode 100644 index 00000000..6fb5fbbf --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/CMakeLists.txt @@ -0,0 +1,72 @@ +# CMake file for build HeatEqaution_Simple tutorial. +# +# Step 1: Create and enter a build directory: +# +# mkdir build +# cd build +# +# Step 2: Configure with CMake from inside the build directory: +# +# cmake .. #if AMReX not installed +# +# or: +# +# cmake -DAMReX_ROOT=/path/to/installdir .. #if AMReX already installed +# +# Step 3: Build the configuration: +# +# cmake --build . -j4 +# +# Done! You should now see an executable called "HelloWorld". To run: +# +# mpiexec -n 4 ./HeatEqaution_Simple +# +# For additional CMake compile options see +# https://amrex-codes.github.io/amrex/docs_html/BuildingAMReX.html#building-with-cmake + +cmake_minimum_required(VERSION 3.16) + +# Project name and source file languages +project(HeatEquation_Simple + LANGUAGES C CXX) + +add_executable(HeatEquation_Simple main.cpp) + +# To use a pre-installed AMReX build, run: +# cmake -DAMReX_ROOT=/path/to/installdir +# Otherwise cmake will download AMReX from GitHub + +if(NOT DEFINED AMReX_ROOT) + message("-- Download and configure AMReX from GitHub") + + #Download AMReX from GitHub + include(FetchContent) + set(FETCHCONTENT_QUIET OFF) + + FetchContent_Declare( + amrex_code + GIT_REPOSITORY https://github.com/AMReX-Codes/amrex.git/ + GIT_TAG origin/development + ) + + FetchContent_Populate(amrex_code) + + # CMake will read the files in these directories to configure, build + # and install AMReX. + add_subdirectory(${amrex_code_SOURCE_DIR} ${amrex_code_BINARY_DIR}) + +else() + + # Add AMReX install + message("-- Searching for AMReX install directory at ${AMReX_ROOT}") + find_package(AMReX PATHS ${AMReX_ROOT}/lib/cmake/AMReX/AMReXConfig.cmake) + +endif() + +target_link_libraries(HeatEquation_Simple PRIVATE AMReX::amrex) + +## Copy input files to build directory +file( COPY ${CMAKE_SOURCE_DIR}/inputs DESTINATION . ) + +## Copy jupyter notebook for visulation to build directory +file( COPY ${CMAKE_SOURCE_DIR}/Visualization.ipynb DESTINATION . ) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile new file mode 100644 index 00000000..806f01ba --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile @@ -0,0 +1,16 @@ +# AMREX_HOME defines the directory in which we will find all the AMReX code. +AMREX_HOME ?= ../../../amrex + +DEBUG = FALSE +USE_MPI = FALSE +USE_OMP = FALSE +COMP = gnu +DIM = 3 + +include $(AMREX_HOME)/Tools/GNUMake/Make.defs + +include ./Make.package + +include $(AMREX_HOME)/Src/Base/Make.package + +include $(AMREX_HOME)/Tools/GNUMake/Make.rules diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package new file mode 100644 index 00000000..7f43e5e8 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package @@ -0,0 +1,2 @@ +CEXE_sources += main.cpp + diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Visualization.ipynb b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Visualization.ipynb new file mode 100644 index 00000000..e9dddb47 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Visualization.ipynb @@ -0,0 +1,200 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "839a3961", + "metadata": {}, + "source": [ + "## Example: 3D Heat Equation\n", + "\n", + "### What Features Are We Using\n", + "\n", + "* Mesh data \n", + "* Domain Decomposition\n", + "\n", + "We now look at a more complicated example at and show how simulation results\n", + "can be visualized. This example solves the heat equation,\n", + "\n", + "$$\\frac{\\partial\\phi}{\\partial t} = \\nabla^2\\phi$$\n", + "\n", + "using forward Euler temporal integration on a periodic domain. We could use a\n", + "5-point (in 2D) or 7-point (in 3D) stencil, but for demonstration purposes we\n", + "spatially discretize the PDE by first constructing (negative) fluxes on cell faces, e.g.,\n", + "\n", + "$$F_{i+^1\\!/_2,\\,j} = \\frac{\\phi_{i+1,j}-\\phi_{i,j}}{\\Delta x}$$\n", + "\n", + "and then taking the divergence to update the cells:\n", + "\n", + " $$\\phi_{i,\\,j}^{n+1} = \\phi_{i,\\,j}^n\n", + " + \\frac{\\Delta t}{\\Delta x}\\left(F_{i+^1\\!/_2,\\,j}-F_{i-^1\\!/_2,\\,j}\\right)\n", + " + \\frac{\\Delta t}{\\Delta y}\\left(F_{i,\\,j+^1\\!/_2}-F_{i,\\,j-^1\\!/_2}\\right)$$" + ] + }, + { + "cell_type": "markdown", + "id": "08b4ec5f", + "metadata": {}, + "source": [ + "The code to generate the initial condition is in `mykernel.H` and looks like: \n", + "```C++\n", + "{\n", + " Real x = prob_lo[0] + (i+Real(0.5)) * dx[0];\n", + " Real y = prob_lo[1] + (j+Real(0.5)) * dx[1];\n", + " Real z = prob_lo[2] + (k+Real(0.5)) * dx[2];\n", + " Real r2 = ((x-Real(0.25))*(x-Real(0.25))+(y-Real(0.25))*(y-Real(0.25))+(z-Real(0.25))*(z-Real(0.25)))/Real(0.01);\n", + " phi(i,j,k) = Real(1.) + std::exp(-r2);\n", + "}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "33bd71f2", + "metadata": {}, + "source": [ + "## Running the code\n", + "\n", + "The simulation can be ran as `./03_HeatEquation inputs`. \n", + "\n", + "The following inputs parameters could be tweaked:\n", + "\n", + "```\n", + "nsteps = 1000 # number of time steps to take\n", + "plot_int = 100 # write plots every n steps\n", + "n_cell = 128 # number of cells in the domain\n", + "max_grid_size = 64 # max grid size used for domain decomposition\n", + "\n", + "```\n", + "\n", + "Although we are running this example in serial, we decompose the domain into multiple boxes, anticipating more complicated problems where we have mesh refinement:\n", + "\n", + "```C++\n", + " IntVect dom_lo(AMREX_D_DECL( 0, 0, 0));\n", + " IntVect dom_hi(AMREX_D_DECL(n_cell-1, n_cell-1, n_cell-1));\n", + " Box domain(dom_lo, dom_hi);\n", + "\n", + " // Initialize the boxarray \"ba\" from the single box \"bx\"\n", + " ba.define(domain);\n", + " // Break up boxarray \"ba\" into chunks no larger than \"max_grid_size\" along a direction\n", + " ba.maxSize(max_grid_size);\n", + "\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "b772b897", + "metadata": {}, + "source": [ + "## Visualizating the results\n", + "\n", + "Below we give some python code to visualizate the solution using yt:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29609c78", + "metadata": {}, + "outputs": [], + "source": [ + "import yt\n", + "from yt.frontends import boxlib\n", + "from yt.frontends.boxlib.data_structures import AMReXDataset" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18ec883c", + "metadata": {}, + "outputs": [], + "source": [ + "ls" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb533aa1", + "metadata": {}, + "outputs": [], + "source": [ + "ds = AMReXDataset(\"plt00000\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d834eaf", + "metadata": {}, + "outputs": [], + "source": [ + "ds.field_list" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "899767fc", + "metadata": {}, + "outputs": [], + "source": [ + "sl = yt.SlicePlot(ds, 2, ('boxlib', 'phi'))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "679eaca7", + "metadata": {}, + "outputs": [], + "source": [ + "sl" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72af42b5", + "metadata": {}, + "outputs": [], + "source": [ + "ds = AMReXDataset(\"plt01000\")\n", + "sl = yt.SlicePlot(ds, 2, ('boxlib', 'phi'))\n", + "sl" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd7332dd", + "metadata": {}, + "outputs": [], + "source": [ + "sl.annotate_grids()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs new file mode 100644 index 00000000..5b098f82 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs @@ -0,0 +1,7 @@ +n_cell = 32 +max_grid_size = 16 + +nsteps = 1000 +plot_int = 100 + +dt = 1.e-5 diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp new file mode 100644 index 00000000..b3772626 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp @@ -0,0 +1,226 @@ +/* + * A simplified single file version of the HeatEquation_EX0_C exmaple. + * This code is designed to be used with Demo_Tutorial.rst. + * + */ + + +#include +#include +#include + + +int main (int argc, char* argv[]) +{ + amrex::Initialize(argc,argv); + { + + // ********************************** + // DECLARE SIMULATION PARAMETERS + // ********************************** + + // number of cells on each side of the domain + int n_cell; + + // size of each box (or grid) + int max_grid_size; + + // total steps in simulation + int nsteps; + + // how often to write a plotfile + int plot_int; + + // time step + amrex::Real dt; + + // ********************************** + // READ PARAMETER VALUES FROM INPUT DATA + // ********************************** + // inputs parameters + { + // ParmParse is way of reading inputs from the inputs file + // pp.get means we require the inputs file to have it + // pp.query means we optionally need the inputs file to have it - but we must supply a default here + amrex::ParmParse pp; + + // We need to get n_cell from the inputs file - this is the number of cells on each side of + // a square (or cubic) domain. + pp.get("n_cell",n_cell); + + // The domain is broken into boxes of size max_grid_size + pp.get("max_grid_size",max_grid_size); + + // Default nsteps to 10, allow us to set it to something else in the inputs file + nsteps = 10; + pp.query("nsteps",nsteps); + + // Default plot_int to -1, allow us to set it to something else in the inputs file + // If plot_int < 0 then no plot files will be written + plot_int = -1; + pp.query("plot_int",plot_int); + + // time step + pp.get("dt",dt); + } + + // ********************************** + // DEFINE SIMULATION SETUP AND GEOMETRY + // ********************************** + + // make BoxArray and Geometry + // ba will contain a list of boxes that cover the domain + // geom contains information such as the physical domain size, + // number of points in the domain, and periodicity + amrex::BoxArray ba; + amrex::Geometry geom; + + // define lower and upper indices + amrex::IntVect dom_lo(0,0,0); + amrex::IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); + + // Make a single box that is the entire domain + amrex::Box domain(dom_lo, dom_hi); + + // Initialize the boxarray "ba" from the single box "domain" + ba.define(domain); + + // Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction + ba.maxSize(max_grid_size); + + // This defines the physical box, [0,1] in each direction. + amrex::RealBox real_box({ 0., 0., 0.}, + { 1., 1., 1.}); + + // periodic in all direction + amrex::Array is_periodic{1,1,1}; + + // This defines a Geometry object + geom.define(domain, real_box, amrex::CoordSys::cartesian, is_periodic); + + // extract dx from the geometry object + amrex::GpuArray dx = geom.CellSizeArray(); + + // Nghost = number of ghost cells for each array + int Nghost = 1; + + // Ncomp = number of components for each array + int Ncomp = 1; + + // How Boxes are distrubuted among MPI processes + amrex::DistributionMapping dm(ba); + + // we allocate two phi multifabs; one will store the old state, the other the new. + amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); + amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); + + // time = starting time in the simulation + amrex::Real time = 0.0; + + // ********************************** + // INITIALIZE DATA LOOP + // ********************************** + + // loop over boxes + for (amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi) + { + const amrex::Box& bx = mfi.validbox(); + + const amrex::Array4& phiOld = phi_old.array(mfi); + + // set phi = 1 + e^(-(r-0.5)^2) + amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) + { + + // ********************************** + // SET VALUES FOR EACH CELL + // ********************************** + + amrex::Real x = (i+0.5) * dx[0]; + amrex::Real y = (j+0.5) * dx[1]; + amrex::Real z = (k+0.5) * dx[2]; + amrex::Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/0.01; + phiOld(i,j,k) = 1. + std::exp(-rsquared); + }); + } + + // ********************************** + // WRITE INITIAL PLOT FILE + // ********************************** + + // Write a plotfile of the initial data if plot_int > 0 + if (plot_int > 0) + { + int step = 0; + const std::string& pltfile = amrex::Concatenate("plt",step,5); + WriteSingleLevelPlotfile(pltfile, phi_old, {"phi"}, geom, time, 0); + } + + + // ********************************** + // MAIN TIME EVOLUTION LOOP + // ********************************** + + for (int step = 1; step <= nsteps; ++step) + { + // fill periodic ghost cells + phi_old.FillBoundary(geom.periodicity()); + + // new_phi = old_phi + dt * Laplacian(old_phi) + // loop over boxes + for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ) + { + const amrex::Box& bx = mfi.validbox(); + + const amrex::Array4& phiOld = phi_old.array(mfi); + const amrex::Array4& phiNew = phi_new.array(mfi); + + // advance the data by dt + amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) + { + + // ********************************** + // EVOLVE VALUES FOR EACH CELL + // ********************************** + + phiNew(i,j,k) = phiOld(i,j,k) + dt * + ( (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) + +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) + +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]) + ); + }); + } + + // ********************************** + // INCREMENT + // ********************************** + + // update time + time = time + dt; + + // copy new solution into old solution + amrex::MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); + + // Tell the I/O Processor to write out which step we're doing + amrex::Print() << "Advanced step " << step << "\n"; + + + // ********************************** + // WRITE PLOTFILE AT GIVEN INTERVAL + // ********************************** + + // Write a plotfile of the current data (plot_int was defined in the inputs file) + if (plot_int > 0 && step%plot_int == 0) + { + const std::string& pltfile = amrex::Concatenate("plt",step,5); + WriteSingleLevelPlotfile(pltfile, phi_new, {"phi"}, geom, time, step); + } + } + + + } + amrex::Finalize(); + return 0; +} + + From 5621f044d95d5e36fe057fc18573d7844259291d Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 10:30:43 -0700 Subject: [PATCH 034/142] First pass at adding parameters --- .../Case-1/GNUmakefile | 2 +- .../Case-1/functions.sh | 360 ++++++++++++++++++ .../HeatEquation_PythonDriver/Case-1/main.cpp | 80 +++- .../HeatEquation_PythonDriver/Case-1/model.x | 133 +++++++ 4 files changed, 554 insertions(+), 21 deletions(-) create mode 100755 GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh create mode 100755 GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile index 806f01ba..f918d4f2 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile @@ -1,5 +1,5 @@ # AMREX_HOME defines the directory in which we will find all the AMReX code. -AMREX_HOME ?= ../../../amrex +AMREX_HOME ?= ../../../../amrex DEBUG = FALSE USE_MPI = FALSE diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh new file mode 100755 index 00000000..a5eade1a --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh @@ -0,0 +1,360 @@ +#!/bin/bash +# functions.sh - AMReX plotfile processing functions + +# Function to generate output names from plotfile header +function generate_outnames_from_header { + local header_file=$1 + + if [ ! -f "$header_file" ]; then + echo "Error: Header file $header_file not found!" + return 1 + fi + + # Read number of variables from line 2 + local num_variables=$(sed -n '2p' "$header_file") + + # Generate output names file + > outnames.txt + + local line_num=3 + for ((var=0; var> outnames.txt + echo "${var_name}_${comp}_max" >> outnames.txt + done + + echo "Added variable '$var_name' with $num_components components" + done + + echo "Generated outnames.txt with $num_variables variables" +} + +# Function to find which box contains the target ijk coordinates from Cell_H file +function find_box_index { + local target_i=$1 + local target_j=$2 + local target_k=$3 + local cell_header_file=$4 + + if [ ! -f "$cell_header_file" ]; then + echo "Error: Cell header file $cell_header_file not found!" >&2 + echo -1 + return + fi + + # Find the line with box array definition like "(8 0" + local box_array_line=$(grep -n "^([0-9]* [0-9]*$" "$cell_header_file" | cut -d: -f1) + + if [ -z "$box_array_line" ]; then + echo "Error: Could not find box array definition in $cell_header_file" >&2 + echo -1 + return + fi + + # Extract number of boxes from that line + local num_boxes=$(sed -n "${box_array_line}p" "$cell_header_file" | sed 's/^(\([0-9]*\) [0-9]*$/\1/') + + echo "Found $num_boxes boxes in $cell_header_file" >&2 + + # Read each box definition and check if target coordinates fall within + local box_index=0 + local current_line=$((box_array_line + 1)) + + for ((box=0; box&2 + + # Check if target coordinates fall within this box + if [ $target_i -ge $i_lo ] && [ $target_i -le $i_hi ] && \ + [ $target_j -ge $j_lo ] && [ $target_j -le $j_hi ] && \ + [ $target_k -ge $k_lo ] && [ $target_k -le $k_hi ]; then + echo "Target ($target_i,$target_j,$target_k) found in box $box" >&2 + echo $box + return + fi + else + echo "Error: Could not parse box definition: $box_def" >&2 + fi + + ((current_line++)) + done + + # If not found, return -1 + echo "Target coordinates ($target_i,$target_j,$target_k) not found in any box" >&2 + echo -1 +} + +# Function to extract min/max values from cell header +function extract_minmax_from_cell_header { + local plotfile_path=$1 + local level=$2 + local target_i=$3 + local target_j=$4 + local target_k=$5 + local output_names_file=$6 + + local level_header="$plotfile_path/Level_${level}/Cell_H" + + if [ ! -f "$level_header" ]; then + echo "Error: Level header $level_header not found" + return 1 + fi + + # Find which box contains the target coordinates + local box_index=$(find_box_index $target_i $target_j $target_k "$level_header") + + if [ $box_index -eq -1 ]; then + echo "Error: Target coordinates not found in any box" + return 1 + fi + + # Find the min/max sections in the level header + local min_start_line=$(grep -n "^[0-9]*,[0-9]*$" "$level_header" | head -1 | cut -d: -f1) + local max_start_line=$(grep -n "^[0-9]*,[0-9]*$" "$level_header" | tail -1 | cut -d: -f1) + + if [ -z "$min_start_line" ] || [ -z "$max_start_line" ]; then + echo "Error: Could not find min/max sections in level header" + return 1 + fi + + # Get the dimensions from the first line + local dimensions=$(sed -n "${min_start_line}p" "$level_header") + local num_boxes=$(echo $dimensions | cut -d, -f1) + local num_components=$(echo $dimensions | cut -d, -f2) + + # Extract min values (lines after the first dimension line) + local min_end_line=$((min_start_line + num_boxes)) + sed -n "$((min_start_line + 1)),${min_end_line}p" "$level_header" > temp_min.txt + + # Extract max values (lines after the second dimension line) + local max_end_line=$((max_start_line + num_boxes)) + sed -n "$((max_start_line + 1)),${max_end_line}p" "$level_header" > temp_max.txt + + # Get the value from the specific box (line number = box_index + 1) + local target_line=$((box_index + 1)) + + if [ $target_line -gt $num_boxes ]; then + echo "Error: Box index $box_index exceeds available data lines" + rm -f temp_min.txt temp_max.txt + return 1 + fi + + # Extract all component values for the target box + local min_line=$(sed -n "${target_line}p" temp_min.txt) + local max_line=$(sed -n "${target_line}p" temp_max.txt) + + # Parse components (comma-separated) + IFS=',' read -ra min_components <<< "$min_line" + IFS=',' read -ra max_components <<< "$max_line" + + # Read output names + readarray -t output_names < "$output_names_file" + + # Build output line based on requested output names + local output_line="" + for out_name in "${output_names[@]}"; do + # Parse output name like "phi_0_min" or "phi_1_max" + if [[ $out_name =~ ^(.*)_([0-9]+)_(min|max)$ ]]; then + local var_name="${BASH_REMATCH[1]}" + local component="${BASH_REMATCH[2]}" + local min_or_max="${BASH_REMATCH[3]}" + + # Get the value + if [ "$min_or_max" = "min" ]; then + local value="${min_components[$component]}" + else + local value="${max_components[$component]}" + fi + + # Remove trailing comma if present + value="${value%,}" + + output_line+="$value " + fi + done + + # Clean up temp files + rm -f temp_min.txt temp_max.txt + + echo "$output_line" + return 0 +} + +# Function to get the last plotfile in a directory +function get_last_plotfile { + if [ -z $1 ]; then + echo "" + return + else + dir=$1 + fi + + if [ ! -d $dir ]; then + echo "" + return + fi + + plotfileList="" + plotfileNums="" + + # Search for plotfiles with different digit patterns (7, 6, 5 digits) + plotfileList+=" $(find "$dir" -maxdepth 1 -type d -name "*plt???????" | sort -r)" + if [ -d "$dir/output" ]; then + plotfileList+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt???????" | sort -r)" + fi + plotfileNums+=" $(find "$dir" -maxdepth 1 -type d -name "*plt???????" | awk -F/ '{ print $NF }' | sort -r)" + if [ -d "$dir/output" ]; then + plotfileNums+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt???????" | awk -F/ '{ print $NF }' | sort -r)" + fi + + plotfileList+=" $(find "$dir" -maxdepth 1 -type d -name "*plt??????" | sort -r)" + if [ -d "$dir/output" ]; then + plotfileList+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt??????" | sort -r)" + fi + plotfileNums+=" $(find "$dir" -maxdepth 1 -type d -name "*plt??????" | awk -F/ '{ print $NF }' | sort -r)" + if [ -d "$dir/output" ]; then + plotfileNums+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt??????" | awk -F/ '{ print $NF }' | sort -r)" + fi + + plotfileList+=" $(find "$dir" -maxdepth 1 -type d -name "*plt?????" | sort -r)" + if [ -d "$dir/output" ]; then + plotfileList+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt?????" | sort -r)" + fi + plotfileNums+=" $(find "$dir" -maxdepth 1 -type d -name "*plt?????" | awk -F/ '{ print $NF }' | sort -r)" + if [ -d "$dir/output" ]; then + plotfileNums+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt?????" | awk -F/ '{ print $NF }' | sort -r)" + fi + + if [ -z "$plotfileList" ]; then + echo "" + return + fi + + # Find the latest plotfile with a complete Header + for pltNum in $plotfileNums + do + for pltFile in $plotfileList + do + currBaseName=$(echo $pltFile | awk -F/ '{ print $NF }') + + if [ "$currBaseName" == "$pltNum" ]; then + if [ -f ${pltFile}/Header ]; then + plotfile=$pltFile + break + fi + fi + done + + if [ ! -z $plotfile ]; then + break + fi + done + + # Extract out the search directory from the result + plotfile=$(echo ${plotfile#$dir/}) + plotfile=$(echo ${plotfile#$dir}) + + echo $plotfile +} + +# Function to run a single simulation +function run_single_simulation { + local run_counter=$1 + local param_values=("${@:2}") + + # Create subdirectory for this run + local run_dir="run_$(printf "%04d" $run_counter)" + mkdir -p "$run_dir" + + # Change to run directory + cd "$run_dir" + + # Build command line arguments + local cmd_args="" + if [ ${#PARAM_NAMES[@]} -gt 0 ]; then + for i in "${!PARAM_NAMES[@]}"; do + if [ $i -lt ${#param_values[@]} ]; then + cmd_args+="${PARAM_NAMES[$i]}=\"${param_values[$i]}\" " + fi + done + fi + + echo "Running simulation $run_counter with: $cmd_args" + + # Call executable with the parsed inputs + eval "../$EXE $cmd_args" + + # Return to parent directory + cd .. + + # Return the run directory name + echo "$run_dir" +} + +# Function to process a single plotfile and extract data +function process_single_plotfile { + local run_dir=$1 + local run_counter=$2 + + # Get the last plotfile generated + local plotfile=$(get_last_plotfile "$run_dir") + + if [ -z "$plotfile" ] || [ ! -f "$run_dir/$plotfile/Header" ]; then + echo "Error: plotfile header not found in $run_dir" >&2 + return 1 + fi + + local plotfile_path="$run_dir/$plotfile" + + # Generate outnames.txt from the first plotfile if it doesn't exist + if [ ! -f "outnames.txt" ]; then + echo "Generating outnames.txt from plotfile header..." + cd "$run_dir" + generate_outnames_from_header "$plotfile/Header" + mv outnames.txt .. + cd .. + fi + + # Extract min/max values using the function with target coordinates + local result=$(extract_minmax_from_cell_header "$plotfile_path" "$LEVEL" "$TARGET_I" "$TARGET_J" "$TARGET_K" "outnames.txt") + + if [ $? -eq 0 ]; then + echo "$result" + return 0 + else + echo "Error: Failed to extract data from $run_dir" >&2 + return 1 + fi +} + +# Function to initialize output file with header +function initialize_output_file { + local output_file=$1 + + > "$output_file" + if [ -f "outnames.txt" ]; then + readarray -t output_names < outnames.txt + echo "# ${output_names[@]}" >> "$output_file" + fi +} diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp index b3772626..862e6c7c 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp @@ -34,6 +34,19 @@ int main (int argc, char* argv[]) // time step amrex::Real dt; + // ********************************** + // DECLARE PHYSICS PARAMETERS + // ********************************** + + // diffusion coefficient for heat equation + amrex::Real diffusion_coeff; + + // amplitude of initial temperature profile + amrex::Real init_amplitude; + + // width parameter controlling spread of initial profile (variance, not std dev) + amrex::Real init_width; + // ********************************** // READ PARAMETER VALUES FROM INPUT DATA // ********************************** @@ -62,6 +75,23 @@ int main (int argc, char* argv[]) // time step pp.get("dt",dt); + + // ********************************** + // READ PHYSICS PARAMETERS + // ********************************** + + // Diffusion coefficient - controls how fast heat spreads + diffusion_coeff = 1.0; + pp.query("diffusion", diffusion_coeff); // Note: input name is "diffusion" but variable is "diffusion_coeff" + + // Initial temperature amplitude + init_amplitude = 1.0; + pp.query("amplitude", init_amplitude); + + // Width parameter - this is the variance (width²), not standard deviation + // Smaller values = more concentrated, larger values = more spread out + init_width = 0.01; // Note: 0.01 to match your original rsquared/0.01 + pp.query("width", init_width); } // ********************************** @@ -128,19 +158,31 @@ int main (int argc, char* argv[]) const amrex::Array4& phiOld = phi_old.array(mfi); - // set phi = 1 + e^(-(r-0.5)^2) - amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) + // ********************************** + // SET INITIAL TEMPERATURE PROFILE + // ********************************** + // Formula: T = 1 + amplitude * exp(-r^2 / width^2) + // where r is distance from center (0.5, 0.5, 0.5) + // + // Parameters: + // - amplitude: controls peak temperature above baseline (1.0) + // - width: controls spread of initial hot spot + // - smaller width = more concentrated + // - larger width = more spread out + +amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) { - - // ********************************** - // SET VALUES FOR EACH CELL - // ********************************** - + // Calculate physical coordinates of cell center amrex::Real x = (i+0.5) * dx[0]; amrex::Real y = (j+0.5) * dx[1]; amrex::Real z = (k+0.5) * dx[2]; - amrex::Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/0.01; - phiOld(i,j,k) = 1. + std::exp(-rsquared); + + // Calculate squared distance from domain center (0.5, 0.5, 0.5) + // Divide by init_width (which is the variance, not standard deviation) + amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; + + // Set initial temperature profile + phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); }); } @@ -175,19 +217,17 @@ int main (int argc, char* argv[]) const amrex::Array4& phiOld = phi_old.array(mfi); const amrex::Array4& phiNew = phi_new.array(mfi); - // advance the data by dt + // advance the data by dt using heat equation with diffusion coefficient amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) { - - // ********************************** - // EVOLVE VALUES FOR EACH CELL - // ********************************** - - phiNew(i,j,k) = phiOld(i,j,k) + dt * - ( (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) - +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) - +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]) - ); + // Calculate the discrete Laplacian using finite differences + amrex::Real laplacian = + (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) + +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) + +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]); + + // Apply heat equation using diffusion_coeff - matches Python version + phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; }); } diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x new file mode 100755 index 00000000..9f66997c --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x @@ -0,0 +1,133 @@ +#!/bin/bash +# model.x - wrapper script that extracts min/max from plotfile headers + +# Source the functions from external file +# Use the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/functions.sh" + +# Alternative ways to source: +# source ./functions.sh # If functions.sh is in current directory +# . ./functions.sh # Alternative syntax for source +# source /path/to/functions.sh # Absolute path + +# CONFIGURABLE VARIABLES +EXE="main.ex" # Executable name +LEVEL=0 # Default level +TARGET_I=10 # Target i coordinate +TARGET_J=20 # Target j coordinate +TARGET_K=30 # Target k coordinate + +# Create parameter names file +cat > pnames.txt << EOF +input_1 +input_2 +input_3 +input_4 +input_5 +EOF + +# Create parameter marginal PC file for uncertainty quantification +cat > param_margpc.txt << EOF +1 0.3 +3 0.1 +1 0.5 +EOF + +# PC configuration for uncertainty quantification +PC_TYPE=HG # Hermite-Gaussian PC +INPC_ORDER=1 + +# Get positional arguments +INPUT_FILE="$1" +OUTPUT_FILE="$2" + +# Validate arguments +if [ -z "$INPUT_FILE" ] || [ -z "$OUTPUT_FILE" ]; then + echo "Usage: $0 " + echo "Example: $0 ptrain.txt ytrain.txt" + exit 1 +fi + +if [ ! -f "$INPUT_FILE" ]; then + echo "Error: Input file $INPUT_FILE not found!" + exit 1 +fi + +# Check if parameter names file exists +if [ ! -f "pnames.txt" ]; then + echo "Warning: pnames.txt not found! Using default parameter names." +fi + +# Read parameter names if file exists +if [ -f "pnames.txt" ]; then + readarray -t PARAM_NAMES < pnames.txt + echo "Parameter names: ${PARAM_NAMES[@]}" +fi + +echo "Target coordinates: ($TARGET_I,$TARGET_J,$TARGET_K)" +echo "Using executable: $EXE" +echo "Processing level: $LEVEL" + +# Function to process all simulations +function process_all_simulations { + local input_file=$1 + local output_file=$2 + + local run_counter=0 + local output_initialized=false + + # Read input file line by line + while IFS= read -r line; do + # Skip empty lines and comments + [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue + + # Parse parameter values + local param_values=($(echo "$line")) + + if [ ${#param_values[@]} -eq 0 ]; then + echo "Warning: Empty parameter line at run $run_counter, skipping" + continue + fi + + echo "Processing run $run_counter..." + + # Run the simulation + local run_dir=$(run_single_simulation $run_counter "${param_values[@]}") + + if [ $? -eq 0 ] && [ -d "$run_dir" ]; then + # Process the plotfile and extract data + local result=$(process_single_plotfile "$run_dir" $run_counter) + + if [ $? -eq 0 ]; then + # Initialize output file on first successful run + if [ "$output_initialized" = false ]; then + initialize_output_file "$output_file" + output_initialized=true + fi + + # Write result to output file + echo "$result" >> "$output_file" + echo "Run $run_counter completed successfully" + else + echo "Failed to process run $run_counter" + fi + else + echo "Failed to run simulation $run_counter" + fi + + ((run_counter++)) + + done < "$input_file" + + echo "Completed processing $run_counter runs" + echo "Output written to $output_file" + + if [ -f "outnames.txt" ]; then + echo "Output variables:" + cat outnames.txt + fi +} + +# Call the main processing function +process_all_simulations "$INPUT_FILE" "$OUTPUT_FILE" From ab889b27473496f4f259c5fdbdaf01b548a145c4 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 10:48:12 -0700 Subject: [PATCH 035/142] Running --- .../Case-1/functions.sh | 20 +++++-- .../HeatEquation_PythonDriver/Case-1/model.x | 56 ++++++++++++------- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh index a5eade1a..ac9bc8f5 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh @@ -295,21 +295,31 @@ function run_single_simulation { if [ ${#PARAM_NAMES[@]} -gt 0 ]; then for i in "${!PARAM_NAMES[@]}"; do if [ $i -lt ${#param_values[@]} ]; then - cmd_args+="${PARAM_NAMES[$i]}=\"${param_values[$i]}\" " + # Don't quote the values for AMReX command line format + cmd_args+="${PARAM_NAMES[$i]}=${param_values[$i]} " fi done fi echo "Running simulation $run_counter with: $cmd_args" - # Call executable with the parsed inputs - eval "../$EXE $cmd_args" + # Call executable with inputs file and parameters + # Format: ./executable inputs param1=value1 param2=value2 ... + eval "../$EXE ../$INPUTS $cmd_args" + + local exit_code=$? # Return to parent directory cd .. - # Return the run directory name - echo "$run_dir" + if [ $exit_code -eq 0 ]; then + # Return the run directory name + echo "$run_dir" + return 0 + else + echo "Error: Simulation failed with exit code $exit_code" + return 1 + fi } # Function to process a single plotfile and extract data diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x index 9f66997c..1cba9124 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x @@ -2,36 +2,31 @@ # model.x - wrapper script that extracts min/max from plotfile headers # Source the functions from external file -# Use the directory where this script is located SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/functions.sh" -# Alternative ways to source: -# source ./functions.sh # If functions.sh is in current directory -# . ./functions.sh # Alternative syntax for source -# source /path/to/functions.sh # Absolute path - # CONFIGURABLE VARIABLES -EXE="main.ex" # Executable name +EXE="main3d.gnu.ex" # Executable +INPUTS="inputs" # AMReX inputs file LEVEL=0 # Default level -TARGET_I=10 # Target i coordinate -TARGET_J=20 # Target j coordinate -TARGET_K=30 # Target k coordinate +TARGET_I=16 # Target i coordinate (middle of 32^3 grid) +TARGET_J=16 # Target j coordinate +TARGET_K=16 # Target k coordinate # Create parameter names file -cat > pnames.txt << EOF -input_1 -input_2 -input_3 -input_4 -input_5 -EOF +#cat > pnames.txt << EOF +#input_1 +#input_2 +#input_3 +#input_4 +#input_5 +#EOF # Create parameter marginal PC file for uncertainty quantification cat > param_margpc.txt << EOF -1 0.3 -3 0.1 -1 0.5 +1.0 0.3 +1.5 0.2 +0.01 0.005 EOF # PC configuration for uncertainty quantification @@ -46,6 +41,15 @@ OUTPUT_FILE="$2" if [ -z "$INPUT_FILE" ] || [ -z "$OUTPUT_FILE" ]; then echo "Usage: $0 " echo "Example: $0 ptrain.txt ytrain.txt" + echo "" + echo "Heat Equation Test Parameters:" + echo " diffusion_coeff: thermal diffusion coefficient" + echo " init_amplitude: amplitude of initial temperature profile" + echo " init_width: width parameter (variance) of initial profile" + echo "" + echo "Required files:" + echo " $EXE: executable" + echo " $INPUTS: AMReX inputs file" exit 1 fi @@ -54,6 +58,16 @@ if [ ! -f "$INPUT_FILE" ]; then exit 1 fi +if [ ! -f "$EXE" ]; then + echo "Error: Executable $EXE not found!" + exit 1 +fi + +if [ ! -f "$INPUTS" ]; then + echo "Error: Inputs file $INPUTS not found!" + exit 1 +fi + # Check if parameter names file exists if [ ! -f "pnames.txt" ]; then echo "Warning: pnames.txt not found! Using default parameter names." @@ -65,8 +79,10 @@ if [ -f "pnames.txt" ]; then echo "Parameter names: ${PARAM_NAMES[@]}" fi +echo "Heat Equation Test Configuration:" echo "Target coordinates: ($TARGET_I,$TARGET_J,$TARGET_K)" echo "Using executable: $EXE" +echo "Using inputs file: $INPUTS" echo "Processing level: $LEVEL" # Function to process all simulations From 5051de68791e26080738e107d09f08f48eac30ec Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 10:59:05 -0700 Subject: [PATCH 036/142] Does basic stuff --- .../Case-1/functions.sh | 167 ++++++++++-------- .../HeatEquation_PythonDriver/Case-1/model.x | 35 ++-- 2 files changed, 112 insertions(+), 90 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh index ac9bc8f5..803e827b 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh @@ -203,92 +203,67 @@ function extract_minmax_from_cell_header { # Function to get the last plotfile in a directory function get_last_plotfile { - if [ -z $1 ]; then - echo "" - return - else - dir=$1 - fi - - if [ ! -d $dir ]; then + local dir=${1:-.} + + if [ ! -d "$dir" ]; then echo "" return fi - plotfileList="" - plotfileNums="" - - # Search for plotfiles with different digit patterns (7, 6, 5 digits) - plotfileList+=" $(find "$dir" -maxdepth 1 -type d -name "*plt???????" | sort -r)" - if [ -d "$dir/output" ]; then - plotfileList+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt???????" | sort -r)" - fi - plotfileNums+=" $(find "$dir" -maxdepth 1 -type d -name "*plt???????" | awk -F/ '{ print $NF }' | sort -r)" - if [ -d "$dir/output" ]; then - plotfileNums+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt???????" | awk -F/ '{ print $NF }' | sort -r)" - fi - - plotfileList+=" $(find "$dir" -maxdepth 1 -type d -name "*plt??????" | sort -r)" - if [ -d "$dir/output" ]; then - plotfileList+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt??????" | sort -r)" - fi - plotfileNums+=" $(find "$dir" -maxdepth 1 -type d -name "*plt??????" | awk -F/ '{ print $NF }' | sort -r)" - if [ -d "$dir/output" ]; then - plotfileNums+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt??????" | awk -F/ '{ print $NF }' | sort -r)" - fi - - plotfileList+=" $(find "$dir" -maxdepth 1 -type d -name "*plt?????" | sort -r)" - if [ -d "$dir/output" ]; then - plotfileList+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt?????" | sort -r)" - fi - plotfileNums+=" $(find "$dir" -maxdepth 1 -type d -name "*plt?????" | awk -F/ '{ print $NF }' | sort -r)" + echo "Searching for plotfiles in: $dir" >&2 + + # Find all plotfile directories, sorted by name (which sorts numerically for this format) + local plotfiles=$(find "$dir" -maxdepth 1 -type d -name "plt*" | sort -V) + + # Also check output subdirectory if it exists if [ -d "$dir/output" ]; then - plotfileNums+=" $(find "$dir/output" -maxdepth 1 -type d -name "*plt?????" | awk -F/ '{ print $NF }' | sort -r)" + local output_plotfiles=$(find "$dir/output" -maxdepth 1 -type d -name "plt*" | sort -V) + plotfiles="$plotfiles $output_plotfiles" fi - - if [ -z "$plotfileList" ]; then + + echo "Found plotfiles: $plotfiles" >&2 + + if [ -z "$plotfiles" ]; then echo "" return fi # Find the latest plotfile with a complete Header - for pltNum in $plotfileNums - do - for pltFile in $plotfileList - do - currBaseName=$(echo $pltFile | awk -F/ '{ print $NF }') - - if [ "$currBaseName" == "$pltNum" ]; then - if [ -f ${pltFile}/Header ]; then - plotfile=$pltFile - break - fi - fi - done - - if [ ! -z $plotfile ]; then - break + local last_plotfile="" + for pltFile in $plotfiles; do + echo "Checking plotfile: $pltFile" >&2 + if [ -f "${pltFile}/Header" ]; then + last_plotfile=$pltFile + echo "Valid plotfile found: $pltFile" >&2 + else + echo "No Header found in: $pltFile" >&2 fi done - # Extract out the search directory from the result - plotfile=$(echo ${plotfile#$dir/}) - plotfile=$(echo ${plotfile#$dir}) - - echo $plotfile + if [ ! -z "$last_plotfile" ]; then + # Extract just the plotfile name relative to the search directory + local plotfile_name=$(basename "$last_plotfile") + echo "Returning plotfile: $plotfile_name" >&2 + echo "$plotfile_name" + else + echo "No valid plotfiles found" >&2 + echo "" + fi } -# Function to run a single simulation +# Function to run a single simulation with proper output handling function run_single_simulation { local run_counter=$1 local param_values=("${@:2}") # Create subdirectory for this run local run_dir="run_$(printf "%04d" $run_counter)" + echo "Creating directory: $run_dir" >&2 mkdir -p "$run_dir" # Change to run directory cd "$run_dir" + echo "Changed to directory: $(pwd)" >&2 # Build command line arguments local cmd_args="" @@ -301,55 +276,91 @@ function run_single_simulation { done fi - echo "Running simulation $run_counter with: $cmd_args" - - # Call executable with inputs file and parameters - # Format: ./executable inputs param1=value1 param2=value2 ... - eval "../$EXE ../$INPUTS $cmd_args" + # Build the full command + local full_command="../$EXE ../$INPUTS $cmd_args" + echo "Running: $full_command" >&2 + # Run the command and capture output + $full_command > simulation.log 2>&1 local exit_code=$? - # Return to parent directory - cd .. + echo "Simulation exit code: $exit_code" >&2 - if [ $exit_code -eq 0 ]; then - # Return the run directory name + # Check if plotfiles were created (more reliable than exit code for AMReX) + local plotfiles_created=$(find . -maxdepth 1 -type d -name "plt*" | wc -l) + echo "Number of plotfiles created: $plotfiles_created" >&2 + + if [ $plotfiles_created -gt 0 ]; then + echo "Simulation succeeded - plotfiles created" >&2 + echo "Plotfiles found:" >&2 + ls -la plt*/ >&2 + cd .. + # ONLY output the directory name to stdout for capture echo "$run_dir" return 0 else - echo "Error: Simulation failed with exit code $exit_code" + echo "Simulation failed - no plotfiles created" >&2 + echo "Simulation log:" >&2 + cat simulation.log >&2 + cd .. return 1 fi } -# Function to process a single plotfile and extract data +# Function to process plotfile with proper output handling function process_single_plotfile { local run_dir=$1 local run_counter=$2 + echo "Processing plotfile in $run_dir" >&2 + echo "Contents of $run_dir:" >&2 + ls -la "$run_dir/" >&2 + # Get the last plotfile generated local plotfile=$(get_last_plotfile "$run_dir") - if [ -z "$plotfile" ] || [ ! -f "$run_dir/$plotfile/Header" ]; then - echo "Error: plotfile header not found in $run_dir" >&2 + echo "Found plotfile: '$plotfile'" >&2 + + if [ -z "$plotfile" ]; then + echo "Error: No plotfile found in $run_dir" >&2 + return 1 + fi + + if [ ! -f "$run_dir/$plotfile/Header" ]; then + echo "Error: Header not found at $run_dir/$plotfile/Header" >&2 + if [ -d "$run_dir/$plotfile" ]; then + echo "Contents of $run_dir/$plotfile:" >&2 + ls -la "$run_dir/$plotfile/" >&2 + fi return 1 fi local plotfile_path="$run_dir/$plotfile" + echo "Using plotfile path: $plotfile_path" >&2 # Generate outnames.txt from the first plotfile if it doesn't exist if [ ! -f "outnames.txt" ]; then - echo "Generating outnames.txt from plotfile header..." + echo "Generating outnames.txt from plotfile header..." >&2 cd "$run_dir" generate_outnames_from_header "$plotfile/Header" - mv outnames.txt .. + if [ -f "outnames.txt" ]; then + mv outnames.txt .. + echo "Created outnames.txt:" >&2 + cat ../outnames.txt >&2 + else + echo "Error: Failed to create outnames.txt" >&2 + cd .. + return 1 + fi cd .. fi - # Extract min/max values using the function with target coordinates + # Extract min/max values local result=$(extract_minmax_from_cell_header "$plotfile_path" "$LEVEL" "$TARGET_I" "$TARGET_J" "$TARGET_K" "outnames.txt") if [ $? -eq 0 ]; then + echo "Extracted result: '$result'" >&2 + # ONLY output the result to stdout for capture echo "$result" return 0 else @@ -358,13 +369,15 @@ function process_single_plotfile { fi } -# Function to initialize output file with header +# Function to initialize output file function initialize_output_file { local output_file=$1 + echo "Initializing output file: $output_file" >&2 > "$output_file" if [ -f "outnames.txt" ]; then readarray -t output_names < outnames.txt echo "# ${output_names[@]}" >> "$output_file" + echo "Added header to $output_file" >&2 fi } diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x index 1cba9124..8b495849 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x @@ -85,7 +85,7 @@ echo "Using executable: $EXE" echo "Using inputs file: $INPUTS" echo "Processing level: $LEVEL" -# Function to process all simulations +# Function to process all simulations with better debugging function process_all_simulations { local input_file=$1 local output_file=$2 @@ -106,16 +106,24 @@ function process_all_simulations { continue fi - echo "Processing run $run_counter..." + echo "=== Processing run $run_counter ===" + echo "Parameters: ${param_values[@]}" # Run the simulation + echo "Step 1: Running simulation..." local run_dir=$(run_single_simulation $run_counter "${param_values[@]}") + local sim_success=$? - if [ $? -eq 0 ] && [ -d "$run_dir" ]; then - # Process the plotfile and extract data + echo "Simulation result: exit_code=$sim_success, run_dir='$run_dir'" + + if [ $sim_success -eq 0 ] && [ -d "$run_dir" ]; then + echo "Step 2: Processing plotfile..." local result=$(process_single_plotfile "$run_dir" $run_counter) + local process_success=$? + + echo "Process result: exit_code=$process_success, result='$result'" - if [ $? -eq 0 ]; then + if [ $process_success -eq 0 ]; then # Initialize output file on first successful run if [ "$output_initialized" = false ]; then initialize_output_file "$output_file" @@ -124,24 +132,25 @@ function process_all_simulations { # Write result to output file echo "$result" >> "$output_file" - echo "Run $run_counter completed successfully" + echo "✓ Run $run_counter completed successfully" else - echo "Failed to process run $run_counter" + echo "✗ Failed to process plotfile for run $run_counter" fi else - echo "Failed to run simulation $run_counter" + echo "✗ Failed to run simulation $run_counter" fi ((run_counter++)) + echo "" done < "$input_file" echo "Completed processing $run_counter runs" - echo "Output written to $output_file" - - if [ -f "outnames.txt" ]; then - echo "Output variables:" - cat outnames.txt + if [ -f "$output_file" ]; then + echo "Output written to $output_file:" + cat "$output_file" + else + echo "No output file created - all runs failed" fi } From 121bab82485a1790e3824a2de59911ae97519ada Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 11:14:15 -0700 Subject: [PATCH 037/142] Fix outnames --- .../Case-1/functions.sh | 91 +++++++++++++++---- 1 file changed, 72 insertions(+), 19 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh index 803e827b..ef643667 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh @@ -1,6 +1,63 @@ #!/bin/bash # functions.sh - AMReX plotfile processing functions +# Function to generate output names using variable names from Header and components from Cell_H +function generate_outnames_from_combined_sources { + local plotfile_path=$1 + local level=$2 + + local level_header="$plotfile_path/Level_${level}/Cell_H" + local main_header="$plotfile_path/Header" + + if [ ! -f "$level_header" ] || [ ! -f "$main_header" ]; then + echo "Error: Required header files not found" + return 1 + fi + + echo "DEBUG: Reading from Header: $main_header" >&2 + echo "DEBUG: Reading from Cell_H: $level_header" >&2 + + # Get number of variables from Header (line 2) + local num_variables=$(sed -n '2p' "$main_header") + echo "DEBUG: Number of variables from Header: $num_variables" >&2 + + # Get actual number of components from Cell_H data + local dimensions_line=$(grep "^[0-9]*,[0-9]*$" "$level_header" | head -1) + local num_components=$(echo $dimensions_line | cut -d, -f2) + echo "DEBUG: Actual components from Cell_H: $num_components" >&2 + echo "DEBUG: Cell_H dimensions line: $dimensions_line" >&2 + + # Generate output names file + > outnames.txt + + # Read variable names from Header (starting at line 3) + local line_num=3 + for ((var=0; var&2 + + # Skip the next line in Header (which might be dimensions, not components) + ((line_num++)) + local header_value=$(sed -n "${line_num}p" "$main_header") + echo "DEBUG: Skipping Header line $line_num: '$header_value' (probably not components)" >&2 + ((line_num++)) + + # Generate min/max entries using ACTUAL components from Cell_H + for ((comp=0; comp> outnames.txt + echo "${var_name}_${comp}_max" >> outnames.txt + echo "DEBUG: Added ${var_name}_${comp}_min and ${var_name}_${comp}_max" >&2 + done + + echo "Added variable '$var_name' with $num_components actual components" + done + + echo "Generated outnames.txt with $num_variables variables and $num_components components each" + echo "DEBUG: Final outnames.txt contents:" >&2 + cat outnames.txt >&2 +} + # Function to generate output names from plotfile header function generate_outnames_from_header { local header_file=$1 @@ -307,14 +364,12 @@ function run_single_simulation { fi } -# Function to process plotfile with proper output handling +# Function to process plotfile using combined Header/Cell_H approach function process_single_plotfile { local run_dir=$1 local run_counter=$2 echo "Processing plotfile in $run_dir" >&2 - echo "Contents of $run_dir:" >&2 - ls -la "$run_dir/" >&2 # Get the last plotfile generated local plotfile=$(get_last_plotfile "$run_dir") @@ -326,33 +381,32 @@ function process_single_plotfile { return 1 fi - if [ ! -f "$run_dir/$plotfile/Header" ]; then - echo "Error: Header not found at $run_dir/$plotfile/Header" >&2 - if [ -d "$run_dir/$plotfile" ]; then - echo "Contents of $run_dir/$plotfile:" >&2 - ls -la "$run_dir/$plotfile/" >&2 - fi + local plotfile_path="$run_dir/$plotfile" + + # Check required files exist + if [ ! -f "$plotfile_path/Header" ]; then + echo "Error: Header not found at $plotfile_path/Header" >&2 + return 1 + fi + + if [ ! -f "$plotfile_path/Level_${LEVEL}/Cell_H" ]; then + echo "Error: Cell_H not found at $plotfile_path/Level_${LEVEL}/Cell_H" >&2 return 1 fi - local plotfile_path="$run_dir/$plotfile" echo "Using plotfile path: $plotfile_path" >&2 - # Generate outnames.txt from the first plotfile if it doesn't exist + # Generate outnames.txt from combined Header/Cell_H if it doesn't exist if [ ! -f "outnames.txt" ]; then - echo "Generating outnames.txt from plotfile header..." >&2 - cd "$run_dir" - generate_outnames_from_header "$plotfile/Header" + echo "Generating outnames.txt from Header variables + Cell_H components..." >&2 + generate_outnames_from_combined_sources "$plotfile_path" "$LEVEL" if [ -f "outnames.txt" ]; then - mv outnames.txt .. echo "Created outnames.txt:" >&2 - cat ../outnames.txt >&2 + cat outnames.txt >&2 else echo "Error: Failed to create outnames.txt" >&2 - cd .. return 1 fi - cd .. fi # Extract min/max values @@ -360,7 +414,6 @@ function process_single_plotfile { if [ $? -eq 0 ]; then echo "Extracted result: '$result'" >&2 - # ONLY output the result to stdout for capture echo "$result" return 0 else From dc79ada04c7ae8a46210adc41117ce34615a880a Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 11:17:45 -0700 Subject: [PATCH 038/142] Update output format --- .../HeatEquation_PythonDriver/Case-1/functions.sh | 15 ++++++++++----- .../HeatEquation_PythonDriver/Case-1/model.x | 3 +++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh index ef643667..b294f804 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh @@ -10,7 +10,7 @@ function generate_outnames_from_combined_sources { local main_header="$plotfile_path/Header" if [ ! -f "$level_header" ] || [ ! -f "$main_header" ]; then - echo "Error: Required header files not found" + echo "Error: Required header files not found" >&2 return 1 fi @@ -50,10 +50,12 @@ function generate_outnames_from_combined_sources { echo "DEBUG: Added ${var_name}_${comp}_min and ${var_name}_${comp}_max" >&2 done - echo "Added variable '$var_name' with $num_components actual components" + # ALL OUTPUT TO STDERR + echo "Added variable '$var_name' with $num_components actual components" >&2 done - echo "Generated outnames.txt with $num_variables variables and $num_components components each" + # ALL OUTPUT TO STDERR + echo "Generated outnames.txt with $num_variables variables and $num_components components each" >&2 echo "DEBUG: Final outnames.txt contents:" >&2 cat outnames.txt >&2 } @@ -422,15 +424,18 @@ function process_single_plotfile { fi } -# Function to initialize output file +# Updated initialize_output_file function function initialize_output_file { local output_file=$1 echo "Initializing output file: $output_file" >&2 > "$output_file" - if [ -f "outnames.txt" ]; then + + if [ "$INCLUDE_HEADER" = true ] && [ -f "outnames.txt" ]; then readarray -t output_names < outnames.txt echo "# ${output_names[@]}" >> "$output_file" echo "Added header to $output_file" >&2 + else + echo "Created headerless output file: $output_file" >&2 fi } diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x index 8b495849..ea2e50fa 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x @@ -33,6 +33,9 @@ EOF PC_TYPE=HG # Hermite-Gaussian PC INPC_ORDER=1 +# Output file header +INCLUDE_HEADER=true # Set to false for pure numbers + # Get positional arguments INPUT_FILE="$1" OUTPUT_FILE="$2" From 280a5f940a0acc77bbecdedc48c32e4d1cb77fea Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 11:19:57 -0700 Subject: [PATCH 039/142] Style --- .../Case-1/functions.sh | 160 +++++++++--------- 1 file changed, 80 insertions(+), 80 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh index b294f804..761b40ad 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh @@ -5,56 +5,56 @@ function generate_outnames_from_combined_sources { local plotfile_path=$1 local level=$2 - + local level_header="$plotfile_path/Level_${level}/Cell_H" local main_header="$plotfile_path/Header" - + if [ ! -f "$level_header" ] || [ ! -f "$main_header" ]; then echo "Error: Required header files not found" >&2 return 1 fi - + echo "DEBUG: Reading from Header: $main_header" >&2 echo "DEBUG: Reading from Cell_H: $level_header" >&2 - + # Get number of variables from Header (line 2) local num_variables=$(sed -n '2p' "$main_header") echo "DEBUG: Number of variables from Header: $num_variables" >&2 - + # Get actual number of components from Cell_H data local dimensions_line=$(grep "^[0-9]*,[0-9]*$" "$level_header" | head -1) local num_components=$(echo $dimensions_line | cut -d, -f2) echo "DEBUG: Actual components from Cell_H: $num_components" >&2 echo "DEBUG: Cell_H dimensions line: $dimensions_line" >&2 - + # Generate output names file > outnames.txt - + # Read variable names from Header (starting at line 3) local line_num=3 for ((var=0; var&2 - + # Skip the next line in Header (which might be dimensions, not components) ((line_num++)) local header_value=$(sed -n "${line_num}p" "$main_header") echo "DEBUG: Skipping Header line $line_num: '$header_value' (probably not components)" >&2 ((line_num++)) - + # Generate min/max entries using ACTUAL components from Cell_H for ((comp=0; comp> outnames.txt echo "${var_name}_${comp}_max" >> outnames.txt echo "DEBUG: Added ${var_name}_${comp}_min and ${var_name}_${comp}_max" >&2 done - + # ALL OUTPUT TO STDERR echo "Added variable '$var_name' with $num_components actual components" >&2 done - - # ALL OUTPUT TO STDERR + + # ALL OUTPUT TO STDERR echo "Generated outnames.txt with $num_variables variables and $num_components components each" >&2 echo "DEBUG: Final outnames.txt contents:" >&2 cat outnames.txt >&2 @@ -63,75 +63,75 @@ function generate_outnames_from_combined_sources { # Function to generate output names from plotfile header function generate_outnames_from_header { local header_file=$1 - + if [ ! -f "$header_file" ]; then echo "Error: Header file $header_file not found!" return 1 fi - + # Read number of variables from line 2 local num_variables=$(sed -n '2p' "$header_file") - + # Generate output names file > outnames.txt - + local line_num=3 for ((var=0; var> outnames.txt echo "${var_name}_${comp}_max" >> outnames.txt done - + echo "Added variable '$var_name' with $num_components components" done - + echo "Generated outnames.txt with $num_variables variables" } # Function to find which box contains the target ijk coordinates from Cell_H file function find_box_index { local target_i=$1 - local target_j=$2 + local target_j=$2 local target_k=$3 local cell_header_file=$4 - + if [ ! -f "$cell_header_file" ]; then echo "Error: Cell header file $cell_header_file not found!" >&2 echo -1 return fi - + # Find the line with box array definition like "(8 0" local box_array_line=$(grep -n "^([0-9]* [0-9]*$" "$cell_header_file" | cut -d: -f1) - + if [ -z "$box_array_line" ]; then echo "Error: Could not find box array definition in $cell_header_file" >&2 echo -1 return fi - + # Extract number of boxes from that line local num_boxes=$(sed -n "${box_array_line}p" "$cell_header_file" | sed 's/^(\([0-9]*\) [0-9]*$/\1/') - + echo "Found $num_boxes boxes in $cell_header_file" >&2 - + # Read each box definition and check if target coordinates fall within local box_index=0 local current_line=$((box_array_line + 1)) - + for ((box=0; box&2 - + # Check if target coordinates fall within this box if [ $target_i -ge $i_lo ] && [ $target_i -le $i_hi ] && \ [ $target_j -ge $j_lo ] && [ $target_j -le $j_hi ] && \ @@ -155,10 +155,10 @@ function find_box_index { else echo "Error: Could not parse box definition: $box_def" >&2 fi - + ((current_line++)) done - + # If not found, return -1 echo "Target coordinates ($target_i,$target_j,$target_k) not found in any box" >&2 echo -1 @@ -172,64 +172,64 @@ function extract_minmax_from_cell_header { local target_j=$4 local target_k=$5 local output_names_file=$6 - + local level_header="$plotfile_path/Level_${level}/Cell_H" - + if [ ! -f "$level_header" ]; then echo "Error: Level header $level_header not found" return 1 fi - + # Find which box contains the target coordinates local box_index=$(find_box_index $target_i $target_j $target_k "$level_header") - + if [ $box_index -eq -1 ]; then echo "Error: Target coordinates not found in any box" return 1 fi - + # Find the min/max sections in the level header local min_start_line=$(grep -n "^[0-9]*,[0-9]*$" "$level_header" | head -1 | cut -d: -f1) local max_start_line=$(grep -n "^[0-9]*,[0-9]*$" "$level_header" | tail -1 | cut -d: -f1) - + if [ -z "$min_start_line" ] || [ -z "$max_start_line" ]; then echo "Error: Could not find min/max sections in level header" return 1 fi - + # Get the dimensions from the first line local dimensions=$(sed -n "${min_start_line}p" "$level_header") local num_boxes=$(echo $dimensions | cut -d, -f1) local num_components=$(echo $dimensions | cut -d, -f2) - + # Extract min values (lines after the first dimension line) local min_end_line=$((min_start_line + num_boxes)) sed -n "$((min_start_line + 1)),${min_end_line}p" "$level_header" > temp_min.txt - - # Extract max values (lines after the second dimension line) + + # Extract max values (lines after the second dimension line) local max_end_line=$((max_start_line + num_boxes)) sed -n "$((max_start_line + 1)),${max_end_line}p" "$level_header" > temp_max.txt - + # Get the value from the specific box (line number = box_index + 1) local target_line=$((box_index + 1)) - + if [ $target_line -gt $num_boxes ]; then echo "Error: Box index $box_index exceeds available data lines" rm -f temp_min.txt temp_max.txt return 1 fi - + # Extract all component values for the target box local min_line=$(sed -n "${target_line}p" temp_min.txt) local max_line=$(sed -n "${target_line}p" temp_max.txt) - + # Parse components (comma-separated) IFS=',' read -ra min_components <<< "$min_line" IFS=',' read -ra max_components <<< "$max_line" - + # Read output names readarray -t output_names < "$output_names_file" - + # Build output line based on requested output names local output_line="" for out_name in "${output_names[@]}"; do @@ -238,24 +238,24 @@ function extract_minmax_from_cell_header { local var_name="${BASH_REMATCH[1]}" local component="${BASH_REMATCH[2]}" local min_or_max="${BASH_REMATCH[3]}" - + # Get the value if [ "$min_or_max" = "min" ]; then local value="${min_components[$component]}" else local value="${max_components[$component]}" fi - + # Remove trailing comma if present value="${value%,}" - + output_line+="$value " fi done - + # Clean up temp files rm -f temp_min.txt temp_max.txt - + echo "$output_line" return 0 } @@ -263,25 +263,25 @@ function extract_minmax_from_cell_header { # Function to get the last plotfile in a directory function get_last_plotfile { local dir=${1:-.} - + if [ ! -d "$dir" ]; then echo "" return fi echo "Searching for plotfiles in: $dir" >&2 - + # Find all plotfile directories, sorted by name (which sorts numerically for this format) local plotfiles=$(find "$dir" -maxdepth 1 -type d -name "plt*" | sort -V) - + # Also check output subdirectory if it exists if [ -d "$dir/output" ]; then local output_plotfiles=$(find "$dir/output" -maxdepth 1 -type d -name "plt*" | sort -V) plotfiles="$plotfiles $output_plotfiles" fi - + echo "Found plotfiles: $plotfiles" >&2 - + if [ -z "$plotfiles" ]; then echo "" return @@ -314,16 +314,16 @@ function get_last_plotfile { function run_single_simulation { local run_counter=$1 local param_values=("${@:2}") - + # Create subdirectory for this run local run_dir="run_$(printf "%04d" $run_counter)" echo "Creating directory: $run_dir" >&2 mkdir -p "$run_dir" - + # Change to run directory cd "$run_dir" echo "Changed to directory: $(pwd)" >&2 - + # Build command line arguments local cmd_args="" if [ ${#PARAM_NAMES[@]} -gt 0 ]; then @@ -334,21 +334,21 @@ function run_single_simulation { fi done fi - + # Build the full command local full_command="../$EXE ../$INPUTS $cmd_args" echo "Running: $full_command" >&2 - + # Run the command and capture output $full_command > simulation.log 2>&1 local exit_code=$? - + echo "Simulation exit code: $exit_code" >&2 - + # Check if plotfiles were created (more reliable than exit code for AMReX) local plotfiles_created=$(find . -maxdepth 1 -type d -name "plt*" | wc -l) echo "Number of plotfiles created: $plotfiles_created" >&2 - + if [ $plotfiles_created -gt 0 ]; then echo "Simulation succeeded - plotfiles created" >&2 echo "Plotfiles found:" >&2 @@ -370,34 +370,34 @@ function run_single_simulation { function process_single_plotfile { local run_dir=$1 local run_counter=$2 - + echo "Processing plotfile in $run_dir" >&2 - + # Get the last plotfile generated local plotfile=$(get_last_plotfile "$run_dir") - + echo "Found plotfile: '$plotfile'" >&2 - + if [ -z "$plotfile" ]; then echo "Error: No plotfile found in $run_dir" >&2 return 1 fi - + local plotfile_path="$run_dir/$plotfile" - + # Check required files exist if [ ! -f "$plotfile_path/Header" ]; then echo "Error: Header not found at $plotfile_path/Header" >&2 return 1 fi - + if [ ! -f "$plotfile_path/Level_${LEVEL}/Cell_H" ]; then echo "Error: Cell_H not found at $plotfile_path/Level_${LEVEL}/Cell_H" >&2 return 1 fi - + echo "Using plotfile path: $plotfile_path" >&2 - + # Generate outnames.txt from combined Header/Cell_H if it doesn't exist if [ ! -f "outnames.txt" ]; then echo "Generating outnames.txt from Header variables + Cell_H components..." >&2 @@ -410,10 +410,10 @@ function process_single_plotfile { return 1 fi fi - + # Extract min/max values local result=$(extract_minmax_from_cell_header "$plotfile_path" "$LEVEL" "$TARGET_I" "$TARGET_J" "$TARGET_K" "outnames.txt") - + if [ $? -eq 0 ]; then echo "Extracted result: '$result'" >&2 echo "$result" @@ -427,10 +427,10 @@ function process_single_plotfile { # Updated initialize_output_file function function initialize_output_file { local output_file=$1 - + echo "Initializing output file: $output_file" >&2 > "$output_file" - + if [ "$INCLUDE_HEADER" = true ] && [ -f "outnames.txt" ]; then readarray -t output_names < outnames.txt echo "# ${output_names[@]}" >> "$output_file" From ba1562bc2dfa2e7f711436dc2376991c495324f7 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 11:32:34 -0700 Subject: [PATCH 040/142] Tweak functions and add names --- .../Case-1/functions.sh | 27 ++++++++++++++----- .../HeatEquation_PythonDriver/Case-1/model.x | 6 ++--- .../Case-1/pnames.txt | 3 +++ 3 files changed, 27 insertions(+), 9 deletions(-) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/pnames.txt diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh index 761b40ad..198ae437 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh @@ -310,30 +310,45 @@ function get_last_plotfile { fi } -# Function to run a single simulation with proper output handling +# Add debug output to run_single_simulation function function run_single_simulation { local run_counter=$1 local param_values=("${@:2}") - + + # Debug parameter passing + echo "DEBUG: run_counter=$run_counter" >&2 + echo "DEBUG: param_values=(${param_values[@]})" >&2 + echo "DEBUG: PARAM_NAMES=(${PARAM_NAMES[@]})" >&2 + echo "DEBUG: Length of PARAM_NAMES: ${#PARAM_NAMES[@]}" >&2 + echo "DEBUG: Length of param_values: ${#param_values[@]}" >&2 + # Create subdirectory for this run local run_dir="run_$(printf "%04d" $run_counter)" echo "Creating directory: $run_dir" >&2 mkdir -p "$run_dir" - + # Change to run directory cd "$run_dir" echo "Changed to directory: $(pwd)" >&2 - + # Build command line arguments local cmd_args="" if [ ${#PARAM_NAMES[@]} -gt 0 ]; then + echo "DEBUG: Building command arguments..." >&2 for i in "${!PARAM_NAMES[@]}"; do if [ $i -lt ${#param_values[@]} ]; then - # Don't quote the values for AMReX command line format - cmd_args+="${PARAM_NAMES[$i]}=${param_values[$i]} " + local arg="${PARAM_NAMES[$i]}=${param_values[$i]}" + cmd_args+="$arg " + echo "DEBUG: Added argument $i: $arg" >&2 + else + echo "DEBUG: Skipping index $i (out of range)" >&2 fi done + else + echo "DEBUG: PARAM_NAMES array is empty!" >&2 fi + + echo "DEBUG: Final cmd_args='$cmd_args'" >&2 # Build the full command local full_command="../$EXE ../$INPUTS $cmd_args" diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x index ea2e50fa..bbd66648 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x @@ -24,9 +24,9 @@ TARGET_K=16 # Target k coordinate # Create parameter marginal PC file for uncertainty quantification cat > param_margpc.txt << EOF -1.0 0.3 -1.5 0.2 -0.01 0.005 +1.0 0.25 +1.0 0.25 +0.01 0.0025 EOF # PC configuration for uncertainty quantification diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/pnames.txt b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/pnames.txt new file mode 100644 index 00000000..df802fcb --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/pnames.txt @@ -0,0 +1,3 @@ +diffusion_coeff +init_amplitude +init_width From 5820bd3ec947954b62a2b49871feabe00bef1b29 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 29 Sep 2025 12:34:28 -0700 Subject: [PATCH 041/142] Add datalog --- .../Case-1/functions.sh | 115 +++++++++++++++++- .../HeatEquation_PythonDriver/Case-1/main.cpp | 62 +++++++++- .../HeatEquation_PythonDriver/Case-1/model.x | 4 +- 3 files changed, 172 insertions(+), 9 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh index 198ae437..1a251568 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh @@ -1,6 +1,85 @@ #!/bin/bash # functions.sh - AMReX plotfile processing functions +# Function to extract data from datalog.txt +function extract_data_from_datalog { + local run_dir=$1 + local run_counter=$2 + + local datalog_file="$run_dir/datalog.txt" + + echo "Processing datalog in $run_dir" >&2 + + if [ ! -f "$datalog_file" ]; then + echo "Error: datalog.txt not found in $run_dir" >&2 + return 1 + fi + + # Generate outnames from header on first run + if [ $run_counter -eq 0 ] && [ ! -f "outnames.txt" ]; then + echo "Generating outnames from datalog header..." >&2 + generate_outnames_from_datalog_header "$datalog_file" + fi + + echo "Datalog file contents:" >&2 + cat "$datalog_file" >&2 + + # Get the last line of data (skip comment lines) + local last_line=$(grep -v '^#' "$datalog_file" | tail -1) + + if [ -z "$last_line" ]; then + echo "Error: No data lines found in datalog.txt" >&2 + return 1 + fi + + echo "Last data line: $last_line" >&2 + + # Return all the values from the last line + echo "$last_line" + return 0 +} + +# Function to generate outnames from datalog header +function generate_outnames_from_datalog_header { + local datalog_file=$1 + + if [ ! -f "$datalog_file" ]; then + echo "Error: datalog file $datalog_file not found" >&2 + return 1 + fi + + echo "Generating outnames from datalog header: $datalog_file" >&2 + + # Get the header line (starts with #) + local header_line=$(grep '^#' "$datalog_file" | head -1) + + if [ -z "$header_line" ]; then + echo "Error: No header line found in datalog" >&2 + return 1 + fi + + echo "Found header: $header_line" >&2 + + # Remove the # and split into column names + local columns=$(echo "$header_line" | sed 's/^#//' | tr -s ' ' | sed 's/^ *//' | sed 's/ *$//') + + echo "Parsed columns: $columns" >&2 + + # Convert to array and write to outnames.txt + local column_array=($columns) + + > outnames.txt + for col in "${column_array[@]}"; do + echo "$col" >> outnames.txt + echo "Added column: $col" >&2 + done + + echo "Generated outnames.txt:" >&2 + cat outnames.txt >&2 + + return 0 +} + # Function to generate output names using variable names from Header and components from Cell_H function generate_outnames_from_combined_sources { local plotfile_path=$1 @@ -314,23 +393,23 @@ function get_last_plotfile { function run_single_simulation { local run_counter=$1 local param_values=("${@:2}") - + # Debug parameter passing echo "DEBUG: run_counter=$run_counter" >&2 echo "DEBUG: param_values=(${param_values[@]})" >&2 echo "DEBUG: PARAM_NAMES=(${PARAM_NAMES[@]})" >&2 echo "DEBUG: Length of PARAM_NAMES: ${#PARAM_NAMES[@]}" >&2 echo "DEBUG: Length of param_values: ${#param_values[@]}" >&2 - + # Create subdirectory for this run local run_dir="run_$(printf "%04d" $run_counter)" echo "Creating directory: $run_dir" >&2 mkdir -p "$run_dir" - + # Change to run directory cd "$run_dir" echo "Changed to directory: $(pwd)" >&2 - + # Build command line arguments local cmd_args="" if [ ${#PARAM_NAMES[@]} -gt 0 ]; then @@ -347,7 +426,7 @@ function run_single_simulation { else echo "DEBUG: PARAM_NAMES array is empty!" >&2 fi - + echo "DEBUG: Final cmd_args='$cmd_args'" >&2 # Build the full command @@ -381,6 +460,32 @@ function run_single_simulation { fi } +# Updated process function that uses datalog header automatically +function process_single_simulation_datalog { + local run_dir=$1 + local run_counter=$2 + + echo "Processing simulation $run_counter using datalog approach" >&2 + + if [ ! -d "$run_dir" ]; then + echo "Error: Run directory $run_dir not found" >&2 + return 1 + fi + + # Extract all data from datalog (header parsing handled inside) + local result=$(extract_data_from_datalog "$run_dir" $run_counter) + local extract_success=$? + + if [ $extract_success -eq 0 ]; then + echo "Extracted result: '$result'" >&2 + echo "$result" + return 0 + else + echo "Error: Failed to extract data from datalog in $run_dir" >&2 + return 1 + fi +} + # Function to process plotfile using combined Header/Cell_H approach function process_single_plotfile { local run_dir=$1 diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp index 862e6c7c..6e365528 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp @@ -47,6 +47,15 @@ int main (int argc, char* argv[]) // width parameter controlling spread of initial profile (variance, not std dev) amrex::Real init_width; + // ********************************** + // DECLARE DATALOG PARAMETERS + // ********************************** + const int datwidth = 14; + const int datprecision = 6; + const int timeprecision = 13; + int datalog_int = -1; // Interval for regular output (<=0 means no regular output) + bool datalog_final = true; // Write datalog at final step + // ********************************** // READ PARAMETER VALUES FROM INPUT DATA // ********************************** @@ -73,6 +82,11 @@ int main (int argc, char* argv[]) plot_int = -1; pp.query("plot_int",plot_int); + // Default datalog_int to -1, allow us to set it to something else in the inputs file + // If datalog_int < 0 then no plot files will be written + datalog_int = -1; + pp.query("datalog_int",datalog_int); + // time step pp.get("dt",dt); @@ -82,16 +96,16 @@ int main (int argc, char* argv[]) // Diffusion coefficient - controls how fast heat spreads diffusion_coeff = 1.0; - pp.query("diffusion", diffusion_coeff); // Note: input name is "diffusion" but variable is "diffusion_coeff" + pp.query("diffusion_coeff", diffusion_coeff); // Note: input name is "diffusion" but variable is "diffusion_coeff" // Initial temperature amplitude init_amplitude = 1.0; - pp.query("amplitude", init_amplitude); + pp.query("init_amplitude", init_amplitude); // Width parameter - this is the variance (width²), not standard deviation // Smaller values = more concentrated, larger values = more spread out init_width = 0.01; // Note: 0.01 to match your original rsquared/0.01 - pp.query("width", init_width); + pp.query("init_width", init_width); } // ********************************** @@ -186,6 +200,19 @@ amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) }); } + // ********************************** + // WRITE DATALOG FILE + // ********************************** + if (amrex::ParallelDescriptor::IOProcessor() && (datalog_int>0 || datalog_final)) { + std::ofstream datalog("datalog.txt"); // truncate mode to start fresh + datalog << "#" << std::setw(datwidth-1) << " time"; + datalog << std::setw(datwidth) << " max_temp"; + datalog << std::setw(datwidth) << " std_temp"; + datalog << std::setw(datwidth) << " final_step"; + datalog << std::endl; + datalog.close(); + } + // ********************************** // WRITE INITIAL PLOT FILE // ********************************** @@ -244,6 +271,35 @@ amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) // Tell the I/O Processor to write out which step we're doing amrex::Print() << "Advanced step " << step << "\n"; + // ********************************** + // WRITE DATALOG AT GIVEN INTERVAL + // ********************************** + + // Check if we should write datalog + bool write_datalog = false; + if (datalog_final && step == nsteps) { + write_datalog = true; // Write final step + } else if (datalog_int > 0 && step % datalog_int == 0) { + write_datalog = true; // Write every datalog_int steps + } + + if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { + std::ofstream datalog("datalog.txt", std::ios::app); + + // Calculate temperature statistics + amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); + amrex::Real max_temperature = phi_new.max(0); + amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; + amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; + + datalog << std::setw(datwidth) << std::setprecision(timeprecision) << time; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << max_temperature; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << std_temperature; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << step; + datalog << std::endl; + + datalog.close(); + } // ********************************** // WRITE PLOTFILE AT GIVEN INTERVAL diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x index bbd66648..7750cc79 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x @@ -121,7 +121,9 @@ function process_all_simulations { if [ $sim_success -eq 0 ] && [ -d "$run_dir" ]; then echo "Step 2: Processing plotfile..." - local result=$(process_single_plotfile "$run_dir" $run_counter) + # NEW - using datalog + local result=$(process_single_simulation_datalog "$run_dir" $run_counter) + #local result=$(process_single_plotfile "$run_dir" $run_counter) local process_success=$? echo "Process result: exit_code=$process_success, result='$result'" From a026c614717fbd6dedb4a181ac9fda6723f48a42 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 25 Sep 2025 15:55:17 -0700 Subject: [PATCH 042/142] Update model object --- .../Case-1/bindings.H | 15 ++ .../Case-1/bindings.cpp | 192 +++++++++++++++--- .../HeatEquation_PythonDriver/Case-1/main.cpp | 95 ++++++--- 3 files changed, 242 insertions(+), 60 deletions(-) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.H diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.H b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.H new file mode 100644 index 00000000..b547df36 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.H @@ -0,0 +1,15 @@ +#ifndef SIMULATION_RESULT_H +#define SIMULATION_RESULT_H + +struct SimulationResult { + double max_temperature; + double std_temperature; // Add this field if you want it + int final_step; + double final_time; + bool success; +}; + +// Function declaration +SimulationResult heat_equation_main(int argc, char* argv[]); + +#endif // SIMULATION_RESULT_H diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.cpp index c4d1446f..727bbb30 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.cpp +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/bindings.cpp @@ -1,49 +1,177 @@ /* - * Minimal Python bindings for Heat Equation - * - * This provides just the essential one-line interface: - * result = amrex_heat.run_simulation(["./executable", "inputs"]) + * Generic Model Interface with automatic default parameter handling */ #include #include +#include +#include +#include +#include -// Forward declaration from main.cpp -struct SimulationResult { - double max_temperature; - int final_step; - double final_time; - bool success; -}; +namespace py = pybind11; + +/* + * FORWARD DECLARATIONS + * These must match your actual function signatures in main.cpp + */ +#include "bindings.H" SimulationResult heat_equation_main(int argc, char* argv[]); -namespace py = pybind11; +/* + * STANDARD MODEL INTERFACE + */ +class ModelInterface { +public: + virtual std::vector get_pnames() const = 0; + virtual std::vector get_outnames() const = 0; + virtual std::vector> get_param_margpc() const = 0; -PYBIND11_MODULE(amrex_heat, m) { - m.doc() = "Minimal AMReX Heat Equation Python Interface"; - - // Expose SimulationResult struct - py::class_(m, "SimulationResult") - .def_readonly("max_temperature", &SimulationResult::max_temperature) - .def_readonly("final_step", &SimulationResult::final_step) - .def_readonly("final_time", &SimulationResult::final_time) - .def_readonly("success", &SimulationResult::success); - - // Main simulation function - one-liner interface - m.def("run_simulation", [](py::list args) { - // Convert Python list to C++ argc/argv with proper lifetime management - std::vector args_str; - for (auto item : args) { - args_str.push_back(py::str(item)); + // Run simulation - missing parameters automatically filled with defaults + virtual std::map run_single(const std::map& params = {}) = 0; +}; + +/* + * HEAT EQUATION MODEL IMPLEMENTATION + */ +class HeatEquationModel : public ModelInterface { +public: + std::vector get_pnames() const override { + return {"diffusion", "amplitude", "width"}; + } + + std::vector get_outnames() const override { + return {"max_temperature", "std_temperature"}; + } + + std::vector> get_param_margpc() const override { + return { + {1.0, 0.2}, // diffusion: [mean, std_dev] + {1.5, 0.3}, // amplitude: [mean, std_dev] + {0.01, 0.002} // width: [mean, std_dev] + }; + } + + std::map run_single(const std::map& params = {}) override { + auto pnames = get_pnames(); + auto margpc = get_param_margpc(); + + /* + * AUTOMATIC DEFAULT HANDLING + * Start with defaults (means from marginal PC distributions) + * Then override with any user-provided parameters + */ + std::map full_params; + + // Fill in defaults first (use PC means) + for (size_t i = 0; i < pnames.size(); ++i) { + full_params[pnames[i]] = margpc[i][0]; // margpc[i][0] = mean + } + + // Override with any provided parameters + for (const auto& [name, value] : params) { + if (full_params.find(name) != full_params.end()) { + full_params[name] = value; + } else { + throw std::runtime_error("Unknown parameter: " + name); + } + } + + /* + * BUILD COMMAND LINE ARGUMENTS + * Automatically construct args from parameter map + */ + std::vector args_str = {"./HeatEquation_PythonDriver", "inputs"}; + + for (const auto& [name, value] : full_params) { + args_str.push_back(name + "=" + std::to_string(value)); } + /* + * RUN SIMULATION + */ std::vector args_cstr; for (auto& s : args_str) { args_cstr.push_back(const_cast(s.c_str())); } - args_cstr.push_back(nullptr); // Null terminate + args_cstr.push_back(nullptr); + + auto result = heat_equation_main(static_cast(args_cstr.size() - 1), args_cstr.data()); + + if (!result.success) { + throw std::runtime_error("Simulation failed"); + } + + /* + * RETURN RESULTS + * Map output names to values - fixed initialization syntax + */ + std::map output_map; + output_map["max_temperature"] = result.max_temperature; + output_map["std_temperature"] = result.std_temperature; + + return output_map; + } +}; + +/* + * GENERIC BATCH RUNNER + */ +py::array_t run_batch_generic( + ModelInterface& model, + py::array_t params +) { + auto pnames = model.get_pnames(); + auto outnames = model.get_outnames(); + + auto buf = params.request(); + if (buf.ndim != 2 || buf.shape[1] != static_cast(pnames.size())) { + throw std::runtime_error("Parameters must be (N, " + std::to_string(pnames.size()) + ") array"); + } + + size_t n_sims = buf.shape[0]; + double *ptr = static_cast(buf.ptr); + + std::vector shape = {static_cast(n_sims), static_cast(outnames.size())}; + auto result = py::array_t(shape); + auto result_buf = result.request(); + double *result_ptr = static_cast(result_buf.ptr); + + for (size_t i = 0; i < n_sims; ++i) { + // Build parameter map + std::map param_map; + for (size_t j = 0; j < pnames.size(); ++j) { + param_map[pnames[j]] = ptr[i * pnames.size() + j]; + } + + // Run simulation (defaults automatically handled) + auto outputs = model.run_single(param_map); + + // Store results + for (size_t j = 0; j < outnames.size(); ++j) { + result_ptr[i * outnames.size() + j] = outputs[outnames[j]]; + } + } + + return result; +} + +PYBIND11_MODULE(amrex_heat, m) { + m.doc() = "Generic AMReX Model Interface with automatic defaults"; + + py::class_(m, "ModelInterface") + .def("get_pnames", &ModelInterface::get_pnames) + .def("get_outnames", &ModelInterface::get_outnames) + .def("get_param_margpc", &ModelInterface::get_param_margpc) + .def("run_single", &ModelInterface::run_single, + "Run single simulation with optional parameters (missing params use defaults)", + py::arg("params") = py::dict()); + + py::class_(m, "HeatEquationModel") + .def(py::init<>()); - return heat_equation_main(static_cast(args_cstr.size() - 1), args_cstr.data()); - }, "Run the heat equation simulation and return results", py::arg("args")); -} \ No newline at end of file + m.def("run_batch_generic", &run_batch_generic, + "Run batch simulations", + py::arg("model"), py::arg("params")); +} diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp index 5c073e4f..39a299d2 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp @@ -10,17 +10,11 @@ #include #include -// Simple result structure for Python interface -struct SimulationResult { - double max_temperature; - int final_step; - double final_time; - bool success; -}; +#include "bindings.H" SimulationResult heat_equation_main(int argc, char* argv[]) { - SimulationResult result = {0.0, 0, 0.0, false}; + SimulationResult result = {0.0, 0.0, 0, 0.0, false}; amrex::Initialize(argc,argv); { @@ -44,6 +38,19 @@ SimulationResult heat_equation_main(int argc, char* argv[]) // time step amrex::Real dt; + // ********************************** + // DECLARE PHYSICS PARAMETERS + // ********************************** + + // diffusion coefficient for heat equation + amrex::Real diffusion_coeff; + + // amplitude of initial temperature profile + amrex::Real init_amplitude; + + // width parameter controlling spread of initial profile (variance, not std dev) + amrex::Real init_width; + // ********************************** // READ PARAMETER VALUES FROM INPUT DATA // ********************************** @@ -72,6 +79,23 @@ SimulationResult heat_equation_main(int argc, char* argv[]) // time step pp.get("dt",dt); + + // ********************************** + // READ PHYSICS PARAMETERS + // ********************************** + + // Diffusion coefficient - controls how fast heat spreads + diffusion_coeff = 1.0; + pp.query("diffusion", diffusion_coeff); // Note: input name is "diffusion" but variable is "diffusion_coeff" + + // Initial temperature amplitude + init_amplitude = 1.0; + pp.query("amplitude", init_amplitude); + + // Width parameter - this is the variance (width²), not standard deviation + // Smaller values = more concentrated, larger values = more spread out + init_width = 0.01; // Note: 0.01 to match your original rsquared/0.01 + pp.query("width", init_width); } // ********************************** @@ -138,19 +162,31 @@ SimulationResult heat_equation_main(int argc, char* argv[]) const amrex::Array4& phiOld = phi_old.array(mfi); - // set phi = 1 + e^(-(r-0.5)^2) - amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) + // ********************************** + // SET INITIAL TEMPERATURE PROFILE + // ********************************** + // Formula: T = 1 + amplitude * exp(-r^2 / width^2) + // where r is distance from center (0.5, 0.5, 0.5) + // + // Parameters: + // - amplitude: controls peak temperature above baseline (1.0) + // - width: controls spread of initial hot spot + // - smaller width = more concentrated + // - larger width = more spread out + +amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) { - - // ********************************** - // SET VALUES FOR EACH CELL - // ********************************** - + // Calculate physical coordinates of cell center amrex::Real x = (i+0.5) * dx[0]; amrex::Real y = (j+0.5) * dx[1]; amrex::Real z = (k+0.5) * dx[2]; - amrex::Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/0.01; - phiOld(i,j,k) = 1. + std::exp(-rsquared); + + // Calculate squared distance from domain center (0.5, 0.5, 0.5) + // Divide by init_width (which is the variance, not standard deviation) + amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; + + // Set initial temperature profile + phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); }); } @@ -185,19 +221,17 @@ SimulationResult heat_equation_main(int argc, char* argv[]) const amrex::Array4& phiOld = phi_old.array(mfi); const amrex::Array4& phiNew = phi_new.array(mfi); - // advance the data by dt + // advance the data by dt using heat equation with diffusion coefficient amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) { - - // ********************************** - // EVOLVE VALUES FOR EACH CELL - // ********************************** - - phiNew(i,j,k) = phiOld(i,j,k) + dt * - ( (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) - +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) - +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]) - ); + // Calculate the discrete Laplacian using finite differences + amrex::Real laplacian = + (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) + +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) + +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]); + + // Apply heat equation using diffusion_coeff - matches Python version + phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; }); } @@ -233,6 +267,11 @@ SimulationResult heat_equation_main(int argc, char* argv[]) // Get final max temperature and mark success result.max_temperature = phi_new.max(0); + + // Calculate standard deviation of temperature field + amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); + result.std_temperature = std::sqrt(phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp); + result.success = true; } From eb682ec769a1ec4b5c66174ad621baaca129a37b Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 30 Sep 2025 10:56:50 -0700 Subject: [PATCH 043/142] First pass at code example --- Docs/source/HeatEquation_UQ.rst | 722 ++++++++++++++++++++++++++++++++ 1 file changed, 722 insertions(+) create mode 100644 Docs/source/HeatEquation_UQ.rst diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst new file mode 100644 index 00000000..ffcaeabf --- /dev/null +++ b/Docs/source/HeatEquation_UQ.rst @@ -0,0 +1,722 @@ +.. _guided_pytuq_integration: + +PyTUQ Integration with AMReX Applications +========================================== + +.. admonition:: **Time to Complete**: 30-45 minutes + :class: note + + **Prerequisites**: + - Basic knowledge of AMReX build system + - Familiarity with Python/NumPy + - Understanding of uncertainty quantification concepts + + **What you will learn**: + - How to wrap AMReX simulations with PyTUQ's generic model interface + - Converting simulation codes to ``outputs = model(inputs)`` pattern + - Processing simulation outputs for UQ analysis + +Goals +----- + +This tutorial demonstrates how to integrate PyTUQ (Python Uncertainty Quantification Toolkit) with AMReX-based applications. PyTUQ expects models to follow a simple interface: + +.. code-block:: python + + # Generic PyTUQ model interface + outputs = model(inputs) # Both are numpy arrays + # inputs shape: [n_samples, n_parameters] + # outputs shape: [n_samples, n_outputs] + +You will learn to: + +1. Wrap AMReX simulations to provide this interface +2. Extract and format simulation outputs as numpy arrays +3. Choose the appropriate integration approach for your workflow +4. Run sensitivity analysis and inverse modeling using PyTUQ + +Prerequisites and Setup +----------------------- + +Required Dependencies +~~~~~~~~~~~~~~~~~~~~~ + +Install the following components in order: + +.. code-block:: bash + :caption: Complete installation script + + #!/bin/bash + + # For NERSC: module load conda + + # 1. Clone repositories + git clone --recursive --branch v1.0.0z https://github.com/sandialabs/pytuq + git clone --branch 25.10 https://github.com/amrex-codes/pyamrex + git clone --branch 25.10 https://github.com/amrex-codes/amrex + git clone --branch development https://github.com/amrex-codes/amrex-tutorials + # Alternative: git clone --branch add_pybind_interface_test https://github.com/jmsexton03/amrex-tutorials + + # 2. Setup conda environment + # Create conda environment (use -y for non-interactive) + conda create -y --name pyamrex_pytuq python=3.11 --no-default-packages + + # For NERSC (see https://docs.nersc.gov/development/languages/python/nersc-python/#moving-your-conda-setup-to-globalcommonsoftware): + # conda create -y --prefix /global/common/software/myproject/$USER/pyamrex_pytuq python=3.11 + + conda activate pyamrex_pytuq + # For NERSC: conda activate /global/common/software/myproject/$USER/pyamrex_pytuq + + # 3. Build and install pyAMReX (developer install) + cd pyamrex + + # Set environment variable for AMReX source + export AMREX_SRC=$PWD/../amrex + + # Optional: Set compilers explicitly + # export CC=$(which clang) + # export CXX=$(which clang++) + # For GPU support: + # export CUDACXX=$(which nvcc) + # export CUDAHOSTCXX=$(which clang++) + + # Install Python requirements + python3 -m pip install -U -r requirements.txt + python3 -m pip install -v --force-reinstall --no-deps . + + # Build with cmake (includes all dimensions) + cmake -S . -B build -DAMReX_SPACEDIM="1;2;3" -DpyAMReX_amrex_src=$(pwd)/../amrex + cmake --build build --target pip_install -j 8 + + cd ../ + + # 4. Install PyTUQ + cd pytuq + python -m pip install -r requirements.txt + python -m pip install . + conda install -y dill + cd ../ + + # 5. Setup workflow files (optional - for Case 1 examples) + # mkdir rundir + # cd rundir + # tar -xf ~/workflow_uqpc.tar # Obtain from PyTUQ examples + # cd ../ + + # 6. Verify installation + conda list | grep pyamrex # Should show pyamrex 25.10 + conda list | grep pytuq # Should show pytuq 1.0.0z + +.. note:: + + For NERSC users, consider placing your conda environment in ``/global/common/software`` + for better performance and persistence. See the `NERSC Python documentation + `_ + for details. + +.. warning:: + + Ensure version compatibility: + + - AMReX: 25.10 + - pyAMReX: 25.10 + - PyTUQ: v1.0.0z + - amrex-tutorials: development branch (or jmsexton03/add_pybind_interface_test for testing) + +PyTUQ Model Interface Examples +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PyTUQ supports various model types, all following the same interface pattern: + +.. code-block:: python + :caption: Example 1: Linear model from PyTUQ tests + + def linear_model(x, par): + y = par['b'] + x @ par['W'] + return y + + # Usage with PyTUQ + W = np.random.randn(pdim, npt) + b = np.random.randn(npt) + true_model_params = {'W': W, 'b': b} + + # Model wrapper for PyTUQ + model = lambda x: linear_model(x, true_model_params) + outputs = model(inputs) # inputs: [n_samples, pdim] + +.. code-block:: python + :caption: Example 2: Simple function model + + # Direct lambda function + model = lambda x: x[:,0]**4 - 2.*x[:,0]**3 + + # Or as a class method + class Ishigami: + def __call__(self, x): + # Ishigami function implementation + return y + + model = Ishigami() + outputs = model(inputs) + +.. code-block:: python + :caption: Example 3: External executable wrapper + + def model(x): + # Write inputs to file + np.savetxt('inputs.txt', x) + + # Run external simulation + os.system('./model.x inputs.txt outputs.txt') + + # Load and return outputs + y = np.loadtxt('outputs.txt').reshape(x.shape[0], -1) + return y + +Reference PyTUQ Workflow Examples +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PyTUQ provides several workflow examples demonstrating the model interface: + +.. list-table:: + :header-rows: 1 + + * - Analysis Type + - Example File + - Model Interface Pattern + * - Global Sensitivity + - `ex_pcgsa.py `_ + - ``y = model(x)`` with polynomial chaos + * - Inverse Modeling + - `ex_mcmc_fitmodel.py `_ + - ``y_pred = model(params)`` for likelihood evaluation + * - Gaussian Process + - `ex_gp.py `_ + - Surrogate: ``y_approx = gp.predict(x)`` + * - Linear Regression + - `ex_lreg_merr.py `_ + - ``y = X @ beta + error`` + +Input/Output Specifications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PyTUQ expects data as NumPy arrays with specific shapes: + +.. code-block:: python + + # Standard interface for all models + def model(inputs): + """ + Args: + inputs: np.ndarray of shape [n_samples, n_parameters] + + Returns: + outputs: np.ndarray of shape [n_samples, n_outputs] + """ + return outputs + +**Heat Equation Input Parameters** (from AMReX inputs file): + +.. code-block:: text + :caption: Example inputs file with UQ parameters + + # Grid/Domain parameters + n_cell = 32 # number of cells on each side of domain + max_grid_size = 16 # size of each box (or grid) + + # Time stepping parameters + nsteps = 100 # total steps in simulation + dt = 1.e-5 # time step + + # Output control + plot_int = -1 # how often to write plotfile (-1 = no plots) + datalog_int = -1 # how often to write datalog (-1 = no regular output) + + # Physics parameters (these are what we vary for UQ) + diffusion_coeff = 1.0 # diffusion coefficient for heat equation + init_amplitude = 1.0 # amplitude of initial temperature profile + init_width = 0.01 # width parameter (variance, not std dev) + +**Heat Equation Output Format** (datalog): + +.. code-block:: text + :caption: Datalog output format + + # time max_temp std_temp final_step + 0.01 1.09628 0 1000 + +For UQ analysis, we typically vary the physics parameters (``diffusion_coeff``, ``init_amplitude``, ``init_width``) +and extract the outputs (``max_temp``, ``std_temp``) at the final timestep. + +.. warning:: + + Ensure your outputs are well-correlated with the input parameters being varied. + Outputs unaffected by input changes will produce meaningless UQ results. + +Choosing Your Integration Approach +----------------------------------- + +All approaches ultimately provide the same ``outputs = model(inputs)`` interface: + +.. code-block:: text + + Start: Need outputs = model(inputs) interface + │ + ├─ Using Python already? + │ ├─ Yes → Have pyAMReX? + │ │ ├─ Yes → Case 2: Direct Python model + │ │ └─ Using PICMI/WarpX? → Case 4: PICMI wrapper + │ └─ No → Continue to C++ options + │ + └─ C++ Application (need wrapper) + ├─ Centralized outputs and recompling? → Case 1a: Datalog wrapper + ├─ Bash configuration with no C++ code changes? → Case 1b: Bash wrapper + ├─ Fextract or fcompare workflows? → Case 1c: Fextract wrapper + └─ Want Python bindings? → Case 3: Pybind11 wrapper + +Integration Cases +----------------- + +Case 1: C++ Application Wrappers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All three approaches create a Python wrapper providing the ``model(inputs)`` interface. + +**Case 1a: Datalog Wrapper Approach** +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. admonition:: When to use + :class: tip + + Choose when you have centralized output locations and want efficient I/O. + +**Implementation Steps:** + +1. Add datalog output to C++ code: + + .. code-block:: cpp + :caption: main.cpp modifications + + #include + + // After computation + if (amrex::ParallelDescriptor::IOProcessor()) { + amrex::DataLog& datalog = amrex::DataLog::GetDataLog("uq_outputs.dat"); + datalog << max_temperature << " " << avg_temperature << std::endl; + } + +2. Create Python model wrapper: + + .. code-block:: python + :caption: model.py - PyTUQ interface wrapper + + import numpy as np + import subprocess + import os + + def heat_equation_model(inputs): + """ + PyTUQ-compatible model wrapper for AMReX HeatEquation + + Args: + inputs: np.ndarray [n_samples, n_params] + params = [thermal_conductivity, heat_source, initial_temp] + + Returns: + outputs: np.ndarray [n_samples, 2] + outputs = [max_temp, avg_temp] + """ + n_samples = inputs.shape[0] + outputs = np.zeros((n_samples, 2)) + + for i, params in enumerate(inputs): + # Write input file + with open(f'inputs_{i}', 'w') as f: + f.write(f"thermal_conductivity = {params[0]}\n") + f.write(f"heat_source = {params[1]}\n") + f.write(f"initial_temperature = {params[2]}\n") + + # Run simulation + subprocess.run(['./main3d.ex', f'inputs_{i}']) + + # Extract outputs from datalog + data = np.loadtxt('uq_outputs.dat') + outputs[i] = data[-1, :] # Last timestep + + # Cleanup + os.remove(f'inputs_{i}') + os.remove('uq_outputs.dat') + + return outputs + + # Usage with PyTUQ + model = heat_equation_model + # Now can use with any PyTUQ workflow + +3. Integrate with PyTUQ workflows: + + .. code-block:: python + :caption: run_uq_analysis.py + + import pytuq + from model import heat_equation_model + + # Define parameter ranges + param_ranges = np.array([ + [0.5, 1.5], # thermal_conductivity + [1.0, 10.0], # heat_source + [273, 373] # initial_temperature + ]) + + # Run sensitivity analysis (like ex_pcgsa.py) + sa = pytuq.GlobalSensitivity(model=heat_equation_model, + param_ranges=param_ranges) + results = sa.analyze() + +**Case 1b: Plotfile Processing Wrapper** +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. admonition:: When to use + :class: tip + + Best for minimal C++ code changes when plotfiles already exist. + +**Implementation:** + +.. code-block:: python + :caption: model.py - Plotfile wrapper + + def heat_equation_model(inputs): + """Wrapper using plotfile extraction""" + outputs = np.zeros((inputs.shape[0], 2)) + + for i, params in enumerate(inputs): + # Write inputs and run + write_inputs_file(params, f'inputs_{i}') + subprocess.run(['./main3d.gnu.ex', f'inputs_{i}']) + + # Extract from plotfile using bash script + subprocess.run(['./extract_data.sh', 'plt00100', f'output_{i}.txt']) + outputs[i] = np.loadtxt(f'output_{i}.txt') + + # Cleanup + cleanup_files(i) + + return outputs + +.. code-block:: bash + :caption: extract_data.sh + + #!/bin/bash + # Extract max and average from plotfile + PLOTFILE=$1 + OUTPUT=$2 + + fextract.gnu.ex -s slice.dat $PLOTFILE > temp.dat + grep "max temp" temp.dat | awk '{print $3}' > $OUTPUT + grep "avg temp" temp.dat | awk '{print $3}' >> $OUTPUT + +**Case 1c: Fextract Integration Wrapper** +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + :caption: model.py - Fextract wrapper + + def heat_equation_model(inputs): + """Wrapper using fextract utilities""" + outputs = np.zeros((inputs.shape[0], len(output_vars))) + + for i, params in enumerate(inputs): + # Standard run + run_simulation(params, run_id=i) + + # Use fextract functions + max_val = extract_max_value('plt00100', 'temperature') + avg_val = extract_average_value('plt00100', 'temperature') + outputs[i] = [max_val, avg_val] + + return outputs + +Case 2: PyAMReX Direct Integration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. admonition:: When to use + :class: tip + + For Python-based AMReX applications - provides native model interface. + +**Implementation:** + +.. code-block:: python + :caption: HeatEquationModel.py + + import numpy as np + import pyamrex.amrex as amrex + from BaseModel import BaseModel + + class HeatEquationModel(BaseModel): + """PyAMReX model with PyTUQ interface""" + + def __init__(self, geom, default_params): + super().__init__(geom) + self.default_params = default_params + + def __call__(self, inputs): + """ + Direct PyTUQ interface - no wrapper needed + + Args: + inputs: np.ndarray [n_samples, n_params] + + Returns: + outputs: np.ndarray [n_samples, n_outputs] + """ + n_samples = inputs.shape[0] + outputs = np.zeros((n_samples, 2)) + + for i, params in enumerate(inputs): + # Set parameters + self.thermal_conductivity = params[0] + self.heat_source = params[1] + self.initial_temp = params[2] + + # Initialize and run + self.initialize() + self.advance(n_steps=100) + + # Extract outputs directly + outputs[i, 0] = self.get_max_temperature() + outputs[i, 1] = self.get_avg_temperature() + + # Reset for next run + self.reset() + + return outputs + + # Direct usage with PyTUQ + model = HeatEquationModel(geom, default_params) + # model now provides the required interface + +Case 3: C++ with Pybind11 Bindings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. admonition:: When to use + :class: tip + + When you want compiled performance with Python interface. + +**Binding Implementation:** + +.. code-block:: cpp + :caption: bindings.cpp + + #include + #include + #include "HeatEquation.H" + + namespace py = pybind11; + + py::array_t heat_equation_model(py::array_t inputs) { + // Get input dimensions + auto buf = inputs.request(); + double* in_ptr = static_cast(buf.ptr); + size_t n_samples = buf.shape[0]; + size_t n_params = buf.shape[1]; + + // Prepare output array + py::array_t outputs({n_samples, 2}); + auto out_buf = outputs.request(); + double* out_ptr = static_cast(out_buf.ptr); + + // Run simulations + for (size_t i = 0; i < n_samples; ++i) { + double thermal_cond = in_ptr[i * n_params + 0]; + double heat_source = in_ptr[i * n_params + 1]; + double init_temp = in_ptr[i * n_params + 2]; + + HeatEquation sim(thermal_cond, heat_source, init_temp); + sim.run(); + + out_ptr[i * 2 + 0] = sim.get_max_temperature(); + out_ptr[i * 2 + 1] = sim.get_avg_temperature(); + } + + return outputs; + } + + PYBIND11_MODULE(amrex_uq, m) { + m.def("model", &heat_equation_model, + "Heat equation model with PyTUQ interface"); + } + +**Python usage:** + +.. code-block:: python + + import amrex_uq + + # Direct PyTUQ interface from C++ + model = amrex_uq.model + outputs = model(inputs) # Standard interface + +Case 4: PICMI/WarpX Integration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + :caption: warpx_model.py + + from pywarpx import picmi + import numpy as np + + def warpx_model(inputs): + """ + WarpX model with PyTUQ interface + + Args: + inputs: [n_samples, n_params] + params = [E_max, plasma_density, ...] + """ + outputs = np.zeros((inputs.shape[0], 3)) + + for i, params in enumerate(inputs): + # Create simulation with parameters + sim = picmi.Simulation( + E_max=params[0], + n_plasma=params[1], + # ... other parameters + ) + + # Run simulation + sim.step(max_steps=1000) + + # Extract outputs + outputs[i, 0] = sim.get_field_energy()[-1] + outputs[i, 1] = sim.get_particle_energy()[-1] + outputs[i, 2] = sim.get_momentum_spread()[-1] + + return outputs + + # Ready for PyTUQ + model = warpx_model + +Validation and Testing +---------------------- + +Validate your model interface: + +.. code-block:: python + :caption: test_model_interface.py + + import numpy as np + from model import heat_equation_model # Your model + + # Test with random inputs + n_samples = 10 + n_params = 3 + test_inputs = np.random.rand(n_samples, n_params) + + # Rescale to parameter ranges + param_ranges = np.array([[0.5, 1.5], [1.0, 10.0], [273, 373]]) + for i in range(n_params): + test_inputs[:, i] = (param_ranges[i, 1] - param_ranges[i, 0]) * \ + test_inputs[:, i] + param_ranges[i, 0] + + # Test model interface + outputs = heat_equation_model(test_inputs) + + # Validate output shape + assert outputs.shape[0] == n_samples, "Wrong number of samples" + assert outputs.ndim == 2, "Outputs should be 2D array" + + # Check outputs are reasonable + assert np.all(np.isfinite(outputs)), "Outputs contain NaN or Inf" + assert np.all(outputs > 0), "Temperature should be positive" + + print(f"✓ Model interface validated") + print(f" Input shape: {test_inputs.shape}") + print(f" Output shape: {outputs.shape}") + print(f" Output range: [{outputs.min():.2f}, {outputs.max():.2f}]") + +Running Complete UQ Workflows +------------------------------ + +Example workflow combining model with PyTUQ analysis: + +.. code-block:: python + :caption: complete_uq_workflow.py + + import numpy as np + import pytuq + from model import heat_equation_model + + # 1. Define parameter distributions + param_dists = [ + pytuq.UniformDist(0.5, 1.5), # thermal_conductivity + pytuq.UniformDist(1.0, 10.0), # heat_source + pytuq.UniformDist(273, 373) # initial_temperature + ] + + # 2. Global Sensitivity Analysis (like ex_pcgsa.py) + pce = pytuq.PCE(model=heat_equation_model, + distributions=param_dists, + order=3) + pce.fit(n_samples=100) + sobol_indices = pce.get_sobol_indices() + + # 3. Inverse Modeling (like ex_mcmc_fitmodel.py) + observed_data = np.array([350.0, 325.0]) # Observed [max_temp, avg_temp] + + def likelihood(params): + pred = heat_equation_model(params.reshape(1, -1))[0] + return -0.5 * np.sum((pred - observed_data)**2 / sigma**2) + + mcmc = pytuq.MCMC(likelihood, param_dists) + samples = mcmc.sample(n_samples=1000) + + # 4. Surrogate Modeling (like ex_gp.py) + gp = pytuq.GaussianProcess(model=heat_equation_model, + param_ranges=param_ranges) + gp.fit(n_samples=50) + + # Fast predictions with surrogate + test_points = np.random.rand(1000, 3) + fast_predictions = gp.predict(test_points) + +Troubleshooting +--------------- + +Common Issues and Solutions +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + + * - Issue + - Solution + * - Model returns wrong shape + - Ensure output is ``[n_samples, n_outputs]``, use ``reshape(-1, n_outputs)`` + * - Model crashes on certain parameters + - Add parameter validation and bounds checking in wrapper + * - Slow execution for many samples + - Consider parallel execution or surrogate models + * - Outputs uncorrelated with inputs + - Verify parameters are actually being used in simulation + * - Memory issues with large runs + - Process in batches, clean up temporary files + +Additional Resources +-------------------- + +- `PyTUQ Documentation `_ +- `PyTUQ Examples Repository `_ +- `AMReX Documentation `_ +- `pyAMReX Documentation `_ +- :ref:`guided_heat_equation` - Base tutorial this builds upon + +.. seealso:: + + For complete working examples of the ``outputs = model(inputs)`` pattern, see: + + - ``amrex-tutorials/GuidedTutorials/UQ/Case1a/`` - Datalog wrapper + - ``amrex-tutorials/GuidedTutorials/UQ/Case2/`` - PyAMReX native + - ``amrex-tutorials/GuidedTutorials/UQ/Case3/`` - Pybind11 wrapper From ec849a63c6f9aa2b1e8b62c28d2a5fc50634ecd5 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 30 Sep 2025 11:19:08 -0700 Subject: [PATCH 044/142] Add inputs --- Docs/source/HeatEquation_UQ.rst | 208 ++++++------------ .../HeatEquation_PythonDriver/Case-1/inputs | 21 +- 2 files changed, 82 insertions(+), 147 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index ffcaeabf..2e8bfd8d 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -282,160 +282,86 @@ Case 1: C++ Application Wrappers All three approaches create a Python wrapper providing the ``model(inputs)`` interface. -**Case 1a: Datalog Wrapper Approach** -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Common C++ Modifications for HeatEquation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. admonition:: When to use - :class: tip - - Choose when you have centralized output locations and want efficient I/O. - -**Implementation Steps:** - -1. Add datalog output to C++ code: - - .. code-block:: cpp - :caption: main.cpp modifications - - #include - - // After computation - if (amrex::ParallelDescriptor::IOProcessor()) { - amrex::DataLog& datalog = amrex::DataLog::GetDataLog("uq_outputs.dat"); - datalog << max_temperature << " " << avg_temperature << std::endl; - } - -2. Create Python model wrapper: - - .. code-block:: python - :caption: model.py - PyTUQ interface wrapper - - import numpy as np - import subprocess - import os - - def heat_equation_model(inputs): - """ - PyTUQ-compatible model wrapper for AMReX HeatEquation - - Args: - inputs: np.ndarray [n_samples, n_params] - params = [thermal_conductivity, heat_source, initial_temp] - - Returns: - outputs: np.ndarray [n_samples, 2] - outputs = [max_temp, avg_temp] - """ - n_samples = inputs.shape[0] - outputs = np.zeros((n_samples, 2)) - - for i, params in enumerate(inputs): - # Write input file - with open(f'inputs_{i}', 'w') as f: - f.write(f"thermal_conductivity = {params[0]}\n") - f.write(f"heat_source = {params[1]}\n") - f.write(f"initial_temperature = {params[2]}\n") - - # Run simulation - subprocess.run(['./main3d.ex', f'inputs_{i}']) - - # Extract outputs from datalog - data = np.loadtxt('uq_outputs.dat') - outputs[i] = data[-1, :] # Last timestep - - # Cleanup - os.remove(f'inputs_{i}') - os.remove('uq_outputs.dat') - - return outputs - - # Usage with PyTUQ - model = heat_equation_model - # Now can use with any PyTUQ workflow - -3. Integrate with PyTUQ workflows: - - .. code-block:: python - :caption: run_uq_analysis.py - - import pytuq - from model import heat_equation_model - - # Define parameter ranges - param_ranges = np.array([ - [0.5, 1.5], # thermal_conductivity - [1.0, 10.0], # heat_source - [273, 373] # initial_temperature - ]) - - # Run sensitivity analysis (like ex_pcgsa.py) - sa = pytuq.GlobalSensitivity(model=heat_equation_model, - param_ranges=param_ranges) - results = sa.analyze() - -**Case 1b: Plotfile Processing Wrapper** -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +All C++ HeatEquation cases (1a, 1b, 1c) share the same parameter modifications: -.. admonition:: When to use - :class: tip +**Physics Parametrization:** - Best for minimal C++ code changes when plotfiles already exist. +.. code-block:: cpp + :caption: Add physics parameters (in main.cpp declarations) + + // diffusion coefficient for heat equation + amrex::Real diffusion_coeff; + + // amplitude of initial temperature profile + amrex::Real init_amplitude; + + // width parameter controlling spread of initial profile (variance, not std dev) + amrex::Real init_width; -**Implementation:** +.. code-block:: cpp + :caption: Read parameters from inputs file (in ParmParse section) + + diffusion_coeff = 1.0; + pp.query("diffusion_coeff", diffusion_coeff); + + init_amplitude = 1.0; + pp.query("init_amplitude", init_amplitude); + + init_width = 0.01; + pp.query("init_width", init_width); -.. code-block:: python - :caption: model.py - Plotfile wrapper +.. code-block:: cpp + :caption: Use parameters in initial conditions + + amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; + phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); - def heat_equation_model(inputs): - """Wrapper using plotfile extraction""" - outputs = np.zeros((inputs.shape[0], 2)) - - for i, params in enumerate(inputs): - # Write inputs and run - write_inputs_file(params, f'inputs_{i}') - subprocess.run(['./main3d.gnu.ex', f'inputs_{i}']) - - # Extract from plotfile using bash script - subprocess.run(['./extract_data.sh', 'plt00100', f'output_{i}.txt']) - outputs[i] = np.loadtxt(f'output_{i}.txt') - - # Cleanup - cleanup_files(i) - - return outputs +.. code-block:: cpp + :caption: Use diffusion coefficient in evolution + + phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; -.. code-block:: bash - :caption: extract_data.sh +The cases differ only in how they extract outputs: - #!/bin/bash - # Extract max and average from plotfile - PLOTFILE=$1 - OUTPUT=$2 - - fextract.gnu.ex -s slice.dat $PLOTFILE > temp.dat - grep "max temp" temp.dat | awk '{print $3}' > $OUTPUT - grep "avg temp" temp.dat | awk '{print $3}' >> $OUTPUT +Case 1a: C++ with Datalog Output +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Case 1c: Fextract Integration Wrapper** -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +**Additional Modifications for Datalog:** -.. code-block:: python - :caption: model.py - Fextract wrapper +.. code-block:: cpp + :caption: Add datalog configuration + + const int datwidth = 14; + const int datprecision = 6; + int datalog_int = -1; + bool datalog_final = true; - def heat_equation_model(inputs): - """Wrapper using fextract utilities""" - outputs = np.zeros((inputs.shape[0], len(output_vars))) +.. code-block:: cpp + :caption: Write statistics to datalog.txt + + if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { + std::ofstream datalog("datalog.txt", std::ios::app); - for i, params in enumerate(inputs): - # Standard run - run_simulation(params, run_id=i) - - # Use fextract functions - max_val = extract_max_value('plt00100', 'temperature') - avg_val = extract_average_value('plt00100', 'temperature') - outputs[i] = [max_val, avg_val] + amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); + amrex::Real max_temperature = phi_new.max(0); + amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; + amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; - return outputs + datalog << time << " " << max_temperature << " " << std_temperature << " " << step << std::endl; + } + +Case 1b: C++ with Plotfile/Bash Extraction +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Uses the same parametrized C++ code but with ``plot_int > 0`` to generate plotfiles, then extracts data via bash scripts. + +Case 1c: C++ with Fextract Tools +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Uses the same parametrized C++ code with plotfiles, but extracts data using AMReX fextract utilities. Case 2: PyAMReX Direct Integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs index 5b098f82..2ab554c2 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs @@ -1,7 +1,16 @@ -n_cell = 32 -max_grid_size = 16 +# Grid/Domain parameters +n_cell = 32 # number of cells on each side of domain +max_grid_size = 16 # size of each box (or grid) + +# Time stepping parameters +nsteps = 100 # total steps in simulation +dt = 1.e-5 # time step -nsteps = 1000 -plot_int = 100 - -dt = 1.e-5 +# Output control +plot_int = -1 # how often to write plotfile (-1 = no plots) +datalog_int = -1 # how often to write datalog (-1 = no regular output) + +# Physics parameters (these are what we vary for UQ) +diffusion_coeff = 1.0 # diffusion coefficient for heat equation +init_amplitude = 1.0 # amplitude of initial temperature profile +init_width = 0.01 # width parameter (variance, not std dev) \ No newline at end of file From c80544612888a171cc86f29c1f942c19f41a6430 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 30 Sep 2025 12:04:51 -0700 Subject: [PATCH 045/142] Simpler model scripts --- Docs/source/HeatEquation_UQ.rst | 171 ++++++++++++------ .../Case-1/model_wrapper.sh | 127 +++++++++++++ .../Case-1/postprocess_datalog.sh | 43 +++++ 3 files changed, 284 insertions(+), 57 deletions(-) create mode 100755 GuidedTutorials/HeatEquation_PythonDriver/Case-1/model_wrapper.sh create mode 100755 GuidedTutorials/HeatEquation_PythonDriver/Case-1/postprocess_datalog.sh diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 2e8bfd8d..d83f973d 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -285,83 +285,140 @@ All three approaches create a Python wrapper providing the ``model(inputs)`` int Common C++ Modifications for HeatEquation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -All C++ HeatEquation cases (1a, 1b, 1c) share the same parameter modifications: +All C++ HeatEquation cases (1a, 1b, 1c) require the same parameter modifications: -**Physics Parametrization:** +**Implementation Steps:** -.. code-block:: cpp - :caption: Add physics parameters (in main.cpp declarations) - - // diffusion coefficient for heat equation - amrex::Real diffusion_coeff; - - // amplitude of initial temperature profile - amrex::Real init_amplitude; - - // width parameter controlling spread of initial profile (variance, not std dev) - amrex::Real init_width; +1. Add physics parameters to main.cpp declarations: -.. code-block:: cpp - :caption: Read parameters from inputs file (in ParmParse section) - - diffusion_coeff = 1.0; - pp.query("diffusion_coeff", diffusion_coeff); + .. code-block:: cpp - init_amplitude = 1.0; - pp.query("init_amplitude", init_amplitude); + amrex::Real diffusion_coeff; + amrex::Real init_amplitude; + amrex::Real init_width; + +2. Read parameters from inputs file: + + .. code-block:: cpp - init_width = 0.01; - pp.query("init_width", init_width); + diffusion_coeff = 1.0; + pp.query("diffusion_coeff", diffusion_coeff); + + init_amplitude = 1.0; + pp.query("init_amplitude", init_amplitude); + + init_width = 0.01; + pp.query("init_width", init_width); -.. code-block:: cpp - :caption: Use parameters in initial conditions +3. Use parameters in initial conditions and evolution: + + .. code-block:: cpp - amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; - phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); + // Initial conditions + amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; + phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); + + // Evolution + phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; -.. code-block:: cpp - :caption: Use diffusion coefficient in evolution +.. note:: + + The key to PyTUQ integration is creating a loop that maps input parameters to output quantities. + This loop structure differs between cases: - phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; + - **Case 1**: External Python loop writes inputs files, runs executable, parses outputs + - **Case 2**: Python loop directly calls pyAMReX functions + - **Case 3**: Python loop calls pybind11-wrapped C++ functions -The cases differ only in how they extract outputs: +The cases differ only in output extraction method: Case 1a: C++ with Datalog Output ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Additional Modifications for Datalog:** +.. admonition:: When to use + :class: tip -.. code-block:: cpp - :caption: Add datalog configuration - - const int datwidth = 14; - const int datprecision = 6; - int datalog_int = -1; - bool datalog_final = true; + Choose when you have centralized output locations and want efficient I/O. + +**Implementation Steps:** + +1. Add datalog configuration to C++ code: + + .. code-block:: cpp + + const int datwidth = 14; + const int datprecision = 6; + int datalog_int = -1; + bool datalog_final = true; + pp.query("datalog_int", datalog_int); + +2. Write statistics to datalog.txt: + + .. code-block:: cpp + + // Check if we should write datalog + bool write_datalog = false; + if (datalog_final && step == nsteps) { + write_datalog = true; // Write final step + } else if (datalog_int > 0 && step % datalog_int == 0) { + write_datalog = true; // Write every datalog_int steps + } + + if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { + std::ofstream datalog("datalog.txt", std::ios::app); + + amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); + amrex::Real max_temperature = phi_new.max(0); + amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; + amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; + + datalog << time << " " << max_temperature << " " << std_temperature << " " << step << std::endl; + } + + .. note:: + + Some AMReX codes already have datalog capabilities: + + - For existing datalogs, you may only need to ``tail -n 1 datalog`` for the final values + - For AMR codes, use the built-in DataLog: + + .. code-block:: cpp + + // Assuming 'amr' is your Amr object + if (amrex::ParallelDescriptor::IOProcessor()) { + amr.DataLog(0) << "# time max_temperature mean_temp" << std::endl; + amr.DataLog(0) << time << " " << max_temperature << " " << mean_temp << std::endl; + } + +3. Create Python wrapper to interface with PyTUQ: + + For complete implementation, see: ``amrex-tutorials/ExampleCodes/Basic/HeatEquation_EX1_C/model.py`` + + [Need snippet from you here] + +Case 1b: C++ with Plotfile/Bash Extraction +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. code-block:: cpp - :caption: Write statistics to datalog.txt - - if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { - std::ofstream datalog("datalog.txt", std::ios::app); - - amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); - amrex::Real max_temperature = phi_new.max(0); - amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; - amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; - - datalog << time << " " << max_temperature << " " << std_temperature << " " << step << std::endl; - } +.. admonition:: When to use + :class: tip -Case 1b: C++ with Plotfile/Bash Extraction -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Choose when working with existing AMReX plotfile infrastructure. -Uses the same parametrized C++ code but with ``plot_int > 0`` to generate plotfiles, then extracts data via bash scripts. +**Implementation Steps:** -Case 1c: C++ with Fextract Tools -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +1. Use parametrized C++ code with ``plot_int > 0`` + +2. Create bash extraction script: + + For complete implementation, see: ``amrex-tutorials/ExampleCodes/Basic/HeatEquation_EX1_C/postprocess.sh`` + + [Need snippet from you here] + +3. Create Python wrapper: + + For complete implementation, see: ``amrex-tutorials/ExampleCodes/Basic/HeatEquation_EX1_C/model_bash.py`` -Uses the same parametrized C++ code with plotfiles, but extracts data using AMReX fextract utilities. + [Need snippet from you here] Case 2: PyAMReX Direct Integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model_wrapper.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model_wrapper.sh new file mode 100755 index 00000000..ddc688f5 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model_wrapper.sh @@ -0,0 +1,127 @@ +#!/bin/bash +# model.x - wrapper script for running simulations with datalog extraction + +# Configuration +EXE="main3d.gnu.ex" +INPUTS="inputs" +INCLUDE_HEADER=true +POSTPROCESSOR="./postprocess_datalog.sh" + +# Get arguments +INPUT_FILE="$1" +OUTPUT_FILE="$2" + +# Validate arguments +if [ -z "$INPUT_FILE" ] || [ -z "$OUTPUT_FILE" ]; then + echo "Usage: $0 " + echo "Example: $0 ptrain.txt ytrain.txt" + exit 1 +fi + +# Check required files +for file in "$INPUT_FILE" "$EXE" "$INPUTS" "$POSTPROCESSOR"; do + if [ ! -f "$file" ]; then + echo "Error: Required file $file not found!" + exit 1 + fi +done + +# Make postprocessor executable +chmod +x "$POSTPROCESSOR" + +# Read parameter names if available +if [ -f "pnames.txt" ]; then + readarray -t PARAM_NAMES < pnames.txt + echo "Parameter names: ${PARAM_NAMES[@]}" +fi + +# Function to run single simulation +run_simulation() { + local run_counter=$1 + shift + local param_values=("$@") + + # Create run directory + local run_dir="run_$(printf "%04d" $run_counter)" + mkdir -p "$run_dir" + + # Build command arguments + local cmd_args="" + if [ ${#PARAM_NAMES[@]} -gt 0 ]; then + for i in "${!PARAM_NAMES[@]}"; do + if [ $i -lt ${#param_values[@]} ]; then + cmd_args+="${PARAM_NAMES[$i]}=${param_values[$i]} " + fi + done + fi + + # Run simulation + echo "=== Running simulation $run_counter ===" >&2 + echo "Parameters: ${param_values[@]}" >&2 + + cd "$run_dir" + ../$EXE ../$INPUTS $cmd_args > simulation.log 2>&1 + local exit_code=$? + cd .. + + # Check if simulation produced output + if [ ! -f "$run_dir/datalog.txt" ]; then + echo "Error: Simulation $run_counter failed - no datalog.txt" >&2 + return 1 + fi + + # Call postprocessor + local result=$($POSTPROCESSOR "$run_dir" "$run_counter" "outnames.txt") + + if [ $? -eq 0 ]; then + echo "$result" + return 0 + else + return 1 + fi +} + +# Initialize output file +> "$OUTPUT_FILE" + +# Process all simulations +run_counter=0 +header_written=false + +while IFS= read -r line; do + # Skip empty lines and comments + [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue + + # Parse parameters + param_values=($line) + + if [ ${#param_values[@]} -eq 0 ]; then + continue + fi + + # Run simulation and get result + result=$(run_simulation $run_counter "${param_values[@]}") + + if [ $? -eq 0 ]; then + # Write header on first successful run + if [ "$header_written" = false ] && [ "$INCLUDE_HEADER" = true ] && [ -f "outnames.txt" ]; then + readarray -t output_names < outnames.txt + echo "# ${output_names[@]}" >> "$OUTPUT_FILE" + header_written=true + fi + + # Write result + echo "$result" >> "$OUTPUT_FILE" + echo "✓ Run $run_counter completed" + else + echo "✗ Run $run_counter failed" + fi + + ((run_counter++)) + +done < "$INPUT_FILE" + +echo "Completed processing $run_counter runs" +if [ -f "$OUTPUT_FILE" ]; then + echo "Output written to $OUTPUT_FILE" +fi diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/postprocess_datalog.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/postprocess_datalog.sh new file mode 100755 index 00000000..eee8b6e8 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/postprocess_datalog.sh @@ -0,0 +1,43 @@ +#!/bin/bash +# postprocess_datalog.sh - Extract data from datalog.txt + +# Arguments +RUN_DIR="$1" +RUN_COUNTER="${2:-0}" +OUTNAMES_FILE="${3:-outnames.txt}" + +# Check if run directory exists +if [ ! -d "$RUN_DIR" ]; then + echo "Error: Directory $RUN_DIR not found" >&2 + exit 1 +fi + +DATALOG="$RUN_DIR/datalog.txt" + +# Check if datalog exists +if [ ! -f "$DATALOG" ]; then + echo "Error: $DATALOG not found" >&2 + exit 1 +fi + +# Generate outnames.txt from header on first run +if [ "$RUN_COUNTER" -eq 0 ] && [ ! -f "$OUTNAMES_FILE" ]; then + # Extract header line, remove #, clean up spaces + header=$(grep '^#' "$DATALOG" | head -1 | sed 's/^#//' | tr -s ' ' | sed 's/^ *//' | sed 's/ *$//') + + if [ -n "$header" ]; then + echo "$header" | tr ' ' '\n' > "$OUTNAMES_FILE" + echo "Generated $OUTNAMES_FILE from datalog header" >&2 + fi +fi + +# Extract last non-comment line +result=$(grep -v '^#' "$DATALOG" | tail -1) + +if [ -z "$result" ]; then + echo "Error: No data found in $DATALOG" >&2 + exit 1 +fi + +# Output the result +echo "$result" From d8945e6cf516f13b4fed2dc2447e5a91461d43f1 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 30 Sep 2025 12:26:20 -0700 Subject: [PATCH 046/142] Update docs with configuration --- Docs/source/HeatEquation_UQ.rst | 279 +++++++++++--------------------- 1 file changed, 96 insertions(+), 183 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index d83f973d..03ed3432 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -390,11 +390,24 @@ Case 1a: C++ with Datalog Output amr.DataLog(0) << time << " " << max_temperature << " " << mean_temp << std::endl; } -3. Create Python wrapper to interface with PyTUQ: +3. Configure bash wrapper (``model_wrapper.x``): - For complete implementation, see: ``amrex-tutorials/ExampleCodes/Basic/HeatEquation_EX1_C/model.py`` + .. code-block:: bash + :caption: Configuration section of model_wrapper.x + + # Configuration + EXE="main3d.gnu.ex" + INPUTS="inputs" + INCLUDE_HEADER=true + POSTPROCESSOR="./postprocess_datalog.sh" - [Need snippet from you here] + The ``model_wrapper.x`` script (based on PyTUQ workflow examples) handles: + + - Reading input parameters from file + - Setting up AMReX inputs command line options with parameters + - Running the executable + - Calling postprocessor to extract outputs + - Writing outputs to file Case 1b: C++ with Plotfile/Bash Extraction ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -408,17 +421,77 @@ Case 1b: C++ with Plotfile/Bash Extraction 1. Use parametrized C++ code with ``plot_int > 0`` -2. Create bash extraction script: +3. Configure bash wrapper (``model.x``): + + For plotfile header extraction (current implementation): + + .. code-block:: bash + :caption: Configuration section of model.x for plotfile extraction + + # Source external functions library + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + source "$SCRIPT_DIR/functions.sh" + + # Configuration + EXE="main3d.gnu.ex" # AMReX executable + INPUTS="inputs" # AMReX inputs file + LEVEL=0 # AMReX refinement level to extract from + TARGET_I=16 # Target i coordinate (for 32^3 grid center) + TARGET_J=16 # Target j coordinate + TARGET_K=16 # Target k coordinate + INCLUDE_HEADER=true # Include column headers in output + + The ``model.x`` script with ``functions.sh`` implements plotfile extraction: + + **Workflow:** + + 1. **Run simulation**: Creates run directories and executes AMReX code + + 2. **Locate plotfile**: Finds the last generated plotfile (``plt*``) directory + + 3. **Parse plotfile structure**: + + - Reads ``Header`` file for variable names and component counts + - Reads ``Level_X/Cell_H`` for box definitions and min/max data + - Identifies which box contains target (i,j,k) coordinates + + 4. **Extract min/max values**: + + - For each variable component, extracts minimum and maximum values + - Values are taken from the box containing the target coordinates + - Creates output columns named ``__min/max`` + + **Key functions in functions.sh:** + + - ``process_single_plotfile()``: Main orchestrator for plotfile extraction + - ``get_last_plotfile()``: Finds most recent plotfile in run directory + - ``generate_outnames_from_combined_sources()``: Creates column names from Header/Cell_H + - ``find_box_index()``: Determines which AMReX box contains target coordinates + - ``extract_minmax_from_cell_header()``: Extracts actual min/max values - For complete implementation, see: ``amrex-tutorials/ExampleCodes/Basic/HeatEquation_EX1_C/postprocess.sh`` + **Example plotfile structure:** - [Need snippet from you here] + .. code-block:: text -3. Create Python wrapper: + plt00010/ + ├── Header # Variable names, components, metadata + └── Level_0/ + ├── Cell_H # Box definitions and min/max data + └── Cell_D_00000 # Actual cell data (not used here) - For complete implementation, see: ``amrex-tutorials/ExampleCodes/Basic/HeatEquation_EX1_C/model_bash.py`` + **Output format:** - [Need snippet from you here] + The extraction produces one line per simulation with min/max values for each + variable component at the target location: + + .. code-block:: text + + # phi_0_min phi_0_max phi_1_min phi_1_max ... + 1.234e-05 2.345e-04 3.456e-05 4.567e-04 ... + + This approach extracts summary statistics without reading the full cell data, + making it efficient for uncertainty quantification workflows where only + specific metrics are needed. Case 2: PyAMReX Direct Integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -436,48 +509,12 @@ Case 2: PyAMReX Direct Integration import numpy as np import pyamrex.amrex as amrex from BaseModel import BaseModel - - class HeatEquationModel(BaseModel): - """PyAMReX model with PyTUQ interface""" - - def __init__(self, geom, default_params): - super().__init__(geom) - self.default_params = default_params - - def __call__(self, inputs): - """ - Direct PyTUQ interface - no wrapper needed - - Args: - inputs: np.ndarray [n_samples, n_params] - - Returns: - outputs: np.ndarray [n_samples, n_outputs] - """ - n_samples = inputs.shape[0] - outputs = np.zeros((n_samples, 2)) - - for i, params in enumerate(inputs): - # Set parameters - self.thermal_conductivity = params[0] - self.heat_source = params[1] - self.initial_temp = params[2] - - # Initialize and run - self.initialize() - self.advance(n_steps=100) - - # Extract outputs directly - outputs[i, 0] = self.get_max_temperature() - outputs[i, 1] = self.get_avg_temperature() - - # Reset for next run - self.reset() - - return outputs + + #Define inherited class with __call__ and outnames and pnames methods + class HeatEquationModel(BaseModel) # Direct usage with PyTUQ - model = HeatEquationModel(geom, default_params) + model = HeatEquationModel() # model now provides the required interface Case 3: C++ with Pybind11 Bindings @@ -488,63 +525,15 @@ Case 3: C++ with Pybind11 Bindings When you want compiled performance with Python interface. -**Binding Implementation:** - -.. code-block:: cpp - :caption: bindings.cpp - - #include - #include - #include "HeatEquation.H" - - namespace py = pybind11; - - py::array_t heat_equation_model(py::array_t inputs) { - // Get input dimensions - auto buf = inputs.request(); - double* in_ptr = static_cast(buf.ptr); - size_t n_samples = buf.shape[0]; - size_t n_params = buf.shape[1]; - - // Prepare output array - py::array_t outputs({n_samples, 2}); - auto out_buf = outputs.request(); - double* out_ptr = static_cast(out_buf.ptr); - - // Run simulations - for (size_t i = 0; i < n_samples; ++i) { - double thermal_cond = in_ptr[i * n_params + 0]; - double heat_source = in_ptr[i * n_params + 1]; - double init_temp = in_ptr[i * n_params + 2]; - - HeatEquation sim(thermal_cond, heat_source, init_temp); - sim.run(); - - out_ptr[i * 2 + 0] = sim.get_max_temperature(); - out_ptr[i * 2 + 1] = sim.get_avg_temperature(); - } - - return outputs; - } - - PYBIND11_MODULE(amrex_uq, m) { - m.def("model", &heat_equation_model, - "Heat equation model with PyTUQ interface"); - } - -**Python usage:** - -.. code-block:: python - - import amrex_uq - - # Direct PyTUQ interface from C++ - model = amrex_uq.model - outputs = model(inputs) # Standard interface - Case 4: PICMI/WarpX Integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. admonition:: When to use + :class: tip + + When you want full functionality with a Python main. + + [This example is a simplifiction and should be replaced with a link to picmi inputs] .. code-block:: python :caption: warpx_model.py @@ -582,88 +571,12 @@ Case 4: PICMI/WarpX Integration # Ready for PyTUQ model = warpx_model -Validation and Testing ----------------------- - -Validate your model interface: - -.. code-block:: python - :caption: test_model_interface.py - - import numpy as np - from model import heat_equation_model # Your model - - # Test with random inputs - n_samples = 10 - n_params = 3 - test_inputs = np.random.rand(n_samples, n_params) - - # Rescale to parameter ranges - param_ranges = np.array([[0.5, 1.5], [1.0, 10.0], [273, 373]]) - for i in range(n_params): - test_inputs[:, i] = (param_ranges[i, 1] - param_ranges[i, 0]) * \ - test_inputs[:, i] + param_ranges[i, 0] - - # Test model interface - outputs = heat_equation_model(test_inputs) - - # Validate output shape - assert outputs.shape[0] == n_samples, "Wrong number of samples" - assert outputs.ndim == 2, "Outputs should be 2D array" - - # Check outputs are reasonable - assert np.all(np.isfinite(outputs)), "Outputs contain NaN or Inf" - assert np.all(outputs > 0), "Temperature should be positive" - - print(f"✓ Model interface validated") - print(f" Input shape: {test_inputs.shape}") - print(f" Output shape: {outputs.shape}") - print(f" Output range: [{outputs.min():.2f}, {outputs.max():.2f}]") Running Complete UQ Workflows ------------------------------ Example workflow combining model with PyTUQ analysis: - -.. code-block:: python - :caption: complete_uq_workflow.py - - import numpy as np - import pytuq - from model import heat_equation_model - - # 1. Define parameter distributions - param_dists = [ - pytuq.UniformDist(0.5, 1.5), # thermal_conductivity - pytuq.UniformDist(1.0, 10.0), # heat_source - pytuq.UniformDist(273, 373) # initial_temperature - ] - - # 2. Global Sensitivity Analysis (like ex_pcgsa.py) - pce = pytuq.PCE(model=heat_equation_model, - distributions=param_dists, - order=3) - pce.fit(n_samples=100) - sobol_indices = pce.get_sobol_indices() - - # 3. Inverse Modeling (like ex_mcmc_fitmodel.py) - observed_data = np.array([350.0, 325.0]) # Observed [max_temp, avg_temp] - - def likelihood(params): - pred = heat_equation_model(params.reshape(1, -1))[0] - return -0.5 * np.sum((pred - observed_data)**2 / sigma**2) - - mcmc = pytuq.MCMC(likelihood, param_dists) - samples = mcmc.sample(n_samples=1000) - - # 4. Surrogate Modeling (like ex_gp.py) - gp = pytuq.GaussianProcess(model=heat_equation_model, - param_ranges=param_ranges) - gp.fit(n_samples=50) - - # Fast predictions with surrogate - test_points = np.random.rand(1000, 3) - fast_predictions = gp.predict(test_points) +[add example from pytuq examples] Troubleshooting --------------- @@ -700,6 +613,6 @@ Additional Resources For complete working examples of the ``outputs = model(inputs)`` pattern, see: - - ``amrex-tutorials/GuidedTutorials/UQ/Case1a/`` - Datalog wrapper - - ``amrex-tutorials/GuidedTutorials/UQ/Case2/`` - PyAMReX native - - ``amrex-tutorials/GuidedTutorials/UQ/Case3/`` - Pybind11 wrapper + - ``amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/Case-1/`` - C++ wrappers + - ``amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/Case-2/`` - PyAMReX native + - ``amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/Case-3/`` - Pybind11 wrapper From c0c65e9fa4971472c93e94b6ec71ec237c9391b7 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 30 Sep 2025 12:33:23 -0700 Subject: [PATCH 047/142] Add info about pnames and PC inputs --- Docs/source/HeatEquation_UQ.rst | 34 ++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 03ed3432..21657a6d 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -409,6 +409,22 @@ Case 1a: C++ with Datalog Output - Calling postprocessor to extract outputs - Writing outputs to file +4. Set up files like pnames.txt + diffusion_coeff + init_amplitude + init_width + +5. Use the pytuq infrastructure for setting up the inputs, e.g. + .. code-block:: bash + :caption: Configuration for input PC coefficients + + ## (a) Given mean and standard deviation of each normal random parameter + echo "1 0.1 " > param_margpc.txt + echo "1 0.1" >> param_margpc.txt + echo ".01 0.0025" >> param_margpc.txt + PC_TYPE=HG # Hermite-Gaussian PC + INPC_ORDER=1 + Case 1b: C++ with Plotfile/Bash Extraction ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -421,7 +437,23 @@ Case 1b: C++ with Plotfile/Bash Extraction 1. Use parametrized C++ code with ``plot_int > 0`` -3. Configure bash wrapper (``model.x``): +2. Set up files like pnames.txt + diffusion_coeff + init_amplitude + init_width + +3. Use the pytuq infrastructure for setting up the inputs, e.g. + .. code-block:: bash + :caption: Configuration for input PC coefficients + + ## (a) Given mean and standard deviation of each normal random parameter + echo "1 0.1 " > param_margpc.txt + echo "1 0.1" >> param_margpc.txt + echo ".01 0.0025" >> param_margpc.txt + PC_TYPE=HG # Hermite-Gaussian PC + INPC_ORDER=1 + +4. Configure bash wrapper (``model.x``): For plotfile header extraction (current implementation): From 3624993ac5bf7bbb56838acc4aebf31d165a18ac Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 30 Sep 2025 14:30:14 -0700 Subject: [PATCH 048/142] Change default plot_int --- GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs index 2ab554c2..56ec738e 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs @@ -7,7 +7,7 @@ nsteps = 100 # total steps in simulation dt = 1.e-5 # time step # Output control -plot_int = -1 # how often to write plotfile (-1 = no plots) +plot_int = 100 # how often to write plotfile (-1 = no plots) datalog_int = -1 # how often to write datalog (-1 = no regular output) # Physics parameters (these are what we vary for UQ) From 8081c59435687a8cad93a9c38c9ad27946fced9b Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 30 Sep 2025 15:16:28 -0700 Subject: [PATCH 049/142] Fix links --- Docs/source/HeatEquation_UQ.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 21657a6d..ae7a4770 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -185,16 +185,16 @@ PyTUQ provides several workflow examples demonstrating the model interface: - Example File - Model Interface Pattern * - Global Sensitivity - - `ex_pcgsa.py `_ + - `ex_pcgsa.py `_ - ``y = model(x)`` with polynomial chaos * - Inverse Modeling - - `ex_mcmc_fitmodel.py `_ + - `ex_mcmc_fitmodel.py `_ - ``y_pred = model(params)`` for likelihood evaluation * - Gaussian Process - - `ex_gp.py `_ + - `ex_gp.py `_ - Surrogate: ``y_approx = gp.predict(x)`` * - Linear Regression - - `ex_lreg_merr.py `_ + - `ex_lreg_merr.py `_ - ``y = X @ beta + error`` Input/Output Specifications From a248efae70f82268fd189a081e6a92abb5c8f99e Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 11:33:44 -0700 Subject: [PATCH 050/142] Try inheritance --- .../Case-2/AMReXTorchModel.py | 32 ++ .../Case-2/BaseModel.py | 323 ++++++++++----- .../Case-2/HeatEquationModel.py | 387 ++++++++++++++---- .../HeatEquation_PythonDriver/Case-2/main.py | 22 +- 4 files changed, 563 insertions(+), 201 deletions(-) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXTorchModel.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXTorchModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXTorchModel.py new file mode 100644 index 00000000..498949a5 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXTorchModel.py @@ -0,0 +1,32 @@ +class AMReXTorchModel(AMReXModelBase, nn.Module): + """Full PyTorch nn.Module compatibility with Function parameter handling""" + + def __init__(self, **kwargs): + AMReXModelBase.__init__(self, **kwargs) + nn.Module.__init__(self) + + # Register any learnable parameters if needed + self.register_parameter('scale', nn.Parameter(torch.ones(1))) + self.register_parameter('bias', nn.Parameter(torch.zeros(1))) + + def forward(self, x): + """Enhanced forward with learnable parameters""" + # Get base simulation results + outputs = AMReXModelBase.forward(self, x) + + # Apply learnable transformations if using PyTorch + if isinstance(outputs, torch.Tensor): + outputs = outputs * self.scale + self.bias + + return outputs + + def train_step(self, inputs, targets, optimizer, loss_fn): + """Example training step for PyTorch workflows""" + optimizer.zero_grad() + outputs = self.forward(inputs) + loss = loss_fn(outputs, targets) + loss.backward() + optimizer.step() + return loss.item() + + diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py index d06ce345..251c850c 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py @@ -1,120 +1,239 @@ -#!/usr/bin/env python3 -from abc import ABC, abstractmethod -import numpy as np -from typing import Tuple, List, Optional, Union -import amrex.space3d as amr - - -def load_cupy(): - """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" - if amr.Config.have_gpu: - try: - import cupy as cp - amr.Print("Note: found and will use cupy") - return cp - except ImportError: - amr.Print("Warning: GPU found but cupy not available! Using numpy...") - import numpy as np - return np - if amr.Config.gpu_backend == "SYCL": - amr.Print("Warning: SYCL GPU backend not yet implemented for Python") - import numpy as np - return np - else: - import numpy as np - amr.Print("Note: found and will use numpy") - return np - - -class SimulationModel(ABC): - """Base class defining the simulation interface.""" - - def __init__(self, **kwargs): - """Initialize AMReX if needed.""" +class AMReXModelBase(ModelWrapperFcn): + # Class-level field definitions + _field_info_class = None + _param_fields = [] + _output_fields = [] + _spatial_domain_bounds = None # Spatial domain bounds (separate from parameter bounds) + + def __init__(self, model=None, **kwargs): + # Create modelpar from existing parameter information + modelpar = self._create_modelpar() + + # Setup field info container + self.field_info = self._create_field_info() + + # Setup convenience lists + self.param_names = [f[1] for f in self._param_fields] + self.output_names = [f[1] for f in self._output_fields] + + # Extract parameter bounds and create domain array for Function + param_domain = self._extract_param_domain() + + # Determine dimensions + ndim = kwargs.get('ndim', len(self.param_names)) + outdim = kwargs.get('outdim', len(self.output_names)) + + # Create model function wrapper + if model is None: + model_func = lambda params: self._run_simulation(params) + else: + model_func = model + + # Initialize Function with model + super().__init__( + model_func, + ndim, + modelpar=modelpar, + name=kwargs.get('name', 'AMReXModel') + ) + + # Set the parameter domain using Function's method + if param_domain is not None and len(param_domain) > 0: + self.setDimDom(domain=param_domain) + + # Set output dimension + self.outdim = outdim + + # Setup spatial domain bounds (yt-style) - separate from parameter bounds + if self._spatial_domain_bounds: + self.domain_left_edge = self._spatial_domain_bounds[0] + self.domain_right_edge = self._spatial_domain_bounds[1] + self.domain_dimensions = (self._spatial_domain_bounds[2] + if len(self._spatial_domain_bounds) > 2 else None) + + # Initialize AMReX if needed + self.xp = load_cupy() if not amr.initialized(): amr.initialize([]) - def __call__(self, params: np.ndarray) -> np.ndarray: - """ - Run simulation for each parameter set. + def _create_field_info(self): + """Create yt-style field info container""" + field_info = {} + + for field_tuple in self._param_fields: + field_info[field_tuple] = self._get_field_info(field_tuple) + + for field_tuple in self._output_fields: + field_info[field_tuple] = self._get_field_info(field_tuple) + + return field_info + + def _extract_param_domain(self): + """Extract parameter bounds into Function-compatible domain array""" + domain_list = [] - Parameters: - ----------- - params : np.ndarray of shape (n_samples, n_params) or (n_params,) - Parameter sets to run + for field_tuple in self._param_fields: + info = self._get_field_info(field_tuple) + if 'bounds' in info: + domain_list.append(info['bounds']) + else: + # Use Function's default domain size + dmax = getattr(self, 'dmax', 10.0) + domain_list.append([-dmax, dmax]) + if domain_list: + return np.array(domain_list) + return None + + def forward(self, x): + """ + PyTorch-compatible forward method for inference. + + Args: + x: torch.Tensor or np.ndarray Returns: - -------- - np.ndarray of shape (n_samples, n_outputs) - Simulation outputs + torch.Tensor if input is tensor, else np.ndarray """ + # Check if input is PyTorch tensor + is_torch = False + if hasattr(x, 'detach'): # Duck typing for torch.Tensor + is_torch = True + import torch + x_np = x.detach().cpu().numpy() + else: + x_np = x + + # Run simulation using existing logic + outputs = self._run_simulation(x_np) + + # Convert back to torch if needed + if is_torch: + return torch.from_numpy(outputs).to(x.device) + return outputs + + def __call__(self, x): + """Function interface - routes through forward() for consistency""" + self.checkDim(x) + if hasattr(self, 'domain') and self.domain is not None: + self.checkDomain(x) + return self.forward(x) + + def _run_simulation(self, params): + """Core simulation logic - override in subclasses""" if params.ndim == 1: params = params.reshape(1, -1) n_samples = params.shape[0] - n_outputs = len(self.get_outnames()) - outputs = np.zeros((n_samples, n_outputs)) + outputs = np.zeros((n_samples, self.outdim)) - for i in range(n_samples): - try: + # Default implementation using evolve/postprocess if available + if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): + for i in range(n_samples): multifab, varnames, geom = self.evolve(params[i, :]) outputs[i, :] = self.postprocess(multifab, varnames, geom) - except Exception as e: - amr.Print(f"Warning: Simulation failed for parameter set {i}: {e}") - outputs[i, :] = np.nan + else: + # Must be overridden in subclass + raise NotImplementedError("Must implement _run_simulation or evolve/postprocess") return outputs - @abstractmethod - def evolve(self, param_set: np.ndarray) -> Tuple[amr.MultiFab, Optional[amr.Vector_string], Optional[amr.Geometry]]: - """ - Run a single simulation step/evolution. - - Parameters: - ----------- - param_set : np.ndarray - Single parameter set - - Returns: - -------- - tuple : (multifab, varnames, geom) - - multifab: simulation data - - varnames: variable names or None - - geom: domain geometry or None - """ - pass + @property + def field_list(self): + """All available fields""" + return list(self.field_info.keys()) - def postprocess(self, multifab: amr.MultiFab, varnames: Optional[amr.Vector_string], - geom: Optional[amr.Geometry]) -> np.ndarray: - """ - Basic postprocessing with simple one-line operations. - - Parameters: - ----------- - multifab : amr.MultiFab - Simulation data - varnames : amr.Vector_string or None - Variable names (unused in base implementation) - geom : amr.Geometry or None - Domain geometry (unused in base implementation) - - Returns: - -------- - np.ndarray - Processed outputs [max, sum, l2_norm] - """ - max_val = multifab.max(comp=0, local=False) - sum_val = multifab.sum(comp=0, local=False) - l2_norm = multifab.norm2(0) - - return np.array([max_val, sum_val, l2_norm]) - - @abstractmethod - def get_pnames(self) -> List[str]: - """Get parameter names.""" - pass - - @abstractmethod - def get_outnames(self) -> List[str]: - """Get output names.""" - pass + def _get_field_info(self, field_tuple): + """Override to provide field metadata""" + return {} + + def get_param_info(self, param_name): + """Get info for a specific parameter""" + for field_tuple in self._param_fields: + if field_tuple[1] == param_name: + return self._get_field_info(field_tuple) + return {} + + def get_output_info(self, output_name): + """Get info for a specific output""" + for field_tuple in self._output_fields: + if field_tuple[1] == output_name: + return self._get_field_info(field_tuple) + return {} + + def _create_modelpar(self): + """Create modelpar dictionary with statistical properties""" + modelpar = { + 'param_info': {}, + 'output_info': {}, + 'param_names': [], + 'output_names': [], + 'defaults': {}, + 'bounds': {}, + 'units': {}, + 'mean': [], + 'std': [], + 'distribution': [], # 'normal', 'uniform', 'lognormal', etc. + 'pc_type': 'HG', # Hermite-Gaussian by default + } + + # Extract parameter information including statistical properties + for field_tuple in self._param_fields: + field_type, field_name = field_tuple + info = self._get_field_info(field_tuple) + + modelpar['param_info'][field_name] = info + modelpar['param_names'].append(field_name) + + # Extract statistical properties + if 'mean' in info: + modelpar['mean'].append(info['mean']) + elif 'default' in info: + modelpar['mean'].append(info['default']) + else: + # Use center of bounds if available + if 'bounds' in info: + modelpar['mean'].append(np.mean(info['bounds'])) + else: + modelpar['mean'].append(0.0) + + if 'std' in info: + modelpar['std'].append(info['std']) + else: + # Default to 10% of mean or range + if 'bounds' in info: + # Use range/6 as rough std (99.7% within bounds) + modelpar['std'].append((info['bounds'][1] - info['bounds'][0])/6.0) + else: + modelpar['std'].append(abs(modelpar['mean'][-1]) * 0.1) + + # Store other properties + if 'bounds' in info: + modelpar['bounds'][field_name] = info['bounds'] + if 'units' in info: + modelpar['units'][field_name] = info['units'] + if 'distribution' in info: + modelpar['distribution'].append(info['distribution']) + else: + modelpar['distribution'].append('normal') # default + + # Convert to numpy arrays for easier manipulation + modelpar['mean'] = np.array(modelpar['mean']) + modelpar['std'] = np.array(modelpar['std']) + + # Add output information + for field_tuple in self._output_fields: + field_type, field_name = field_tuple + info = self._get_field_info(field_tuple) + modelpar['output_info'][field_name] = info + modelpar['output_names'].append(field_name) + + return modelpar + + def write_param_marginals(self, filename='param_margpc.txt'): + """Write parameter marginals file for PC analysis""" + with open(filename, 'w') as f: + for i, name in enumerate(self.modelpar['param_names']): + mean = self.modelpar['mean'][i] + std = self.modelpar['std'][i] + f.write(f"{mean} {std}\n") + print(f"Wrote parameter marginals to {filename}") diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py index dd9fc0b5..d19ce1f0 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py @@ -1,19 +1,63 @@ #!/usr/bin/env python3 import numpy as np -from typing import Tuple, List, Optional +from typing import Tuple, Optional import amrex.space3d as amr -from BaseModel import SimulationModel, load_cupy +from AMReXModelBase import AMReXModelBase, load_cupy -class HeatEquationModel(SimulationModel): - """Heat equation simulation model.""" +class HeatEquationModel(AMReXModelBase): + """ + Heat equation simulation model with full statistical properties + and AMReX integration. + """ - def __init__(self, n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 1000, - plot_int: int = 100, dt: float = 1e-5, use_parmparse: bool = False): - super().__init__() - + # Define parameter fields with proper naming convention + _param_fields = [ + ('param', 'diffusion_coefficient'), + ('param', 'initial_amplitude'), + ('param', 'initial_width'), + ] + + # Define output fields + _output_fields = [ + ('output', 'max_temperature'), + ('output', 'mean_temperature'), + ('output', 'std_temperature'), + ('output', 'total_energy'), + ('output', 'center_temperature'), + ] + + # Spatial domain bounds (3D heat equation domain) + _spatial_domain_bounds = [ + np.array([0.0, 0.0, 0.0]), # left edge + np.array([1.0, 1.0, 1.0]), # right edge + np.array([32, 32, 32]) # default grid dimensions + ] + + def __init__(self, n_cell: int = 32, max_grid_size: int = 16, + nsteps: int = 1000, plot_int: int = 100, + dt: float = 1e-5, use_parmparse: bool = False, **kwargs): + """ + Initialize heat equation model. + + Parameters: + ----------- + n_cell : int + Number of grid cells in each dimension + max_grid_size : int + Maximum grid size for AMR + nsteps : int + Number of time steps + plot_int : int + Plot interval (for output control) + dt : float + Time step size + use_parmparse : bool + Whether to use AMReX ParmParse for input + """ + # Store simulation parameters if use_parmparse: - from main import parse_inputs # Import here to avoid circular imports + from main import parse_inputs params = parse_inputs() self.n_cell = params['n_cell'] self.max_grid_size = params['max_grid_size'] @@ -27,29 +71,147 @@ def __init__(self, n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 1000 self.plot_int = plot_int self.dt = dt + # Update spatial domain dimensions based on n_cell + if self.n_cell != 32: + self._spatial_domain_bounds[2] = np.array([self.n_cell, self.n_cell, self.n_cell]) + + # Initialize base class + super().__init__(**kwargs) + + # Store physical constants + self.thermal_conductivity = 1.0 # W/(m·K) + self.specific_heat = 1.0 # J/(kg·K) + self.density = 1.0 # kg/m³ + + def _get_field_info(self, field_tuple): + """ + Provide detailed field metadata with statistical properties. + """ + field_type, field_name = field_tuple + + field_info_dict = { + ('param', 'diffusion_coefficient'): { + 'units': 'm**2/s', + 'display_name': 'Thermal Diffusivity', + 'description': 'α = k/(ρ·cp) - thermal diffusivity coefficient', + 'mean': 1.0, + 'std': 0.3, + 'bounds': [0.1, 5.0], + 'distribution': 'lognormal', # Physical parameter, must be positive + 'log_scale': True, + 'default': 1.0, + 'physical_range': [1e-7, 1e-3], # Realistic range for materials (m²/s) + 'material_examples': { + 'air': 2.2e-5, + 'water': 1.4e-7, + 'steel': 1.2e-5, + 'copper': 1.1e-4, + } + }, + ('param', 'initial_amplitude'): { + 'units': 'K', + 'display_name': 'Initial Temperature Amplitude', + 'description': 'Peak temperature of initial Gaussian distribution', + 'mean': 1.0, + 'std': 0.2, + 'bounds': [0.1, 3.0], + 'distribution': 'normal', + 'log_scale': False, + 'default': 1.0, + 'normalized': True, # Values are normalized to reference temperature + }, + ('param', 'initial_width'): { + 'units': 'm', + 'display_name': 'Initial Distribution Width', + 'description': 'Standard deviation of initial Gaussian temperature field', + 'mean': 0.01, + 'std': 0.003, + 'bounds': [0.001, 0.1], + 'distribution': 'lognormal', # Width must be positive + 'log_scale': True, + 'default': 0.01, + 'relative_to_domain': True, # As fraction of domain size + }, + ('output', 'max_temperature'): { + 'units': 'K', + 'display_name': 'Maximum Temperature', + 'description': 'Maximum temperature in the domain', + 'valid_range': [0.0, np.inf], + 'expected_range': [0.1, 3.0], # Typical range for normalized runs + 'monotonic': 'decreasing', # Decreases with time due to diffusion + }, + ('output', 'mean_temperature'): { + 'units': 'K', + 'display_name': 'Mean Temperature', + 'description': 'Spatial average of temperature field', + 'valid_range': [0.0, np.inf], + 'expected_range': [0.0, 1.0], + 'conserved': True, # Should be conserved in isolated system + }, + ('output', 'std_temperature'): { + 'units': 'K', + 'display_name': 'Temperature Standard Deviation', + 'description': 'Spatial standard deviation of temperature', + 'valid_range': [0.0, np.inf], + 'expected_range': [0.0, 1.0], + 'monotonic': 'decreasing', # Decreases as field homogenizes + }, + ('output', 'total_energy'): { + 'units': 'J', + 'display_name': 'Total Thermal Energy', + 'description': 'Integrated thermal energy in domain', + 'valid_range': [0.0, np.inf], + 'conserved': True, # Conserved quantity + 'scaling': 'extensive', # Scales with system size + }, + ('output', 'center_temperature'): { + 'units': 'K', + 'display_name': 'Center Temperature', + 'description': 'Temperature at domain center', + 'valid_range': [0.0, np.inf], + 'expected_range': [0.0, 3.0], + 'probe_location': [0.5, 0.5, 0.5], # Normalized coordinates + }, + } + + return field_info_dict.get(field_tuple, {}) + def evolve(self, param_set: np.ndarray) -> Tuple[amr.MultiFab, amr.Vector_string, amr.Geometry]: """ - Run heat equation simulation. - + Run heat equation simulation with given parameters. + Parameters: ----------- param_set : np.ndarray - [diffusion_coeff, init_amplitude, init_width] - + [diffusion_coefficient, initial_amplitude, initial_width] + Returns: -------- tuple : (phi_new, varnames, geom) - Ready to pass to write_single_level_plotfile + MultiFab with solution, variable names, and geometry """ - from main import main # Import here to avoid circular imports - - if len(param_set) != 3: - raise ValueError(f"Expected 3 parameters, got {len(param_set)}") - + from main import main # Import simulation driver + + if len(param_set) != len(self.param_names): + raise ValueError(f"Expected {len(self.param_names)} parameters, got {len(param_set)}") + + # Extract parameters with proper names + diffusion_coeff = float(param_set[0]) + init_amplitude = float(param_set[1]) + init_width = float(param_set[2]) + + # Validate parameters against bounds + for i, (param_val, param_name) in enumerate(zip(param_set, self.param_names)): + info = self.get_param_info(param_name) + if 'bounds' in info: + if param_val < info['bounds'][0] or param_val > info['bounds'][1]: + print(f"Warning: {param_name}={param_val} outside bounds {info['bounds']}") + + # Run simulation phi_new, geom = main( - diffusion_coeff=float(param_set[0]), - init_amplitude=float(param_set[1]), - init_width=float(param_set[2]), + diffusion_coeff=diffusion_coeff, + init_amplitude=init_amplitude, + init_width=init_width, n_cell=self.n_cell, max_grid_size=self.max_grid_size, nsteps=self.nsteps, @@ -58,128 +220,177 @@ def evolve(self, param_set: np.ndarray) -> Tuple[amr.MultiFab, amr.Vector_string plot_files_output=False, verbose=0 ) - - varnames = amr.Vector_string(['phi']) + + varnames = amr.Vector_string(['temperature']) return phi_new, varnames, geom - def postprocess(self, multifab: amr.MultiFab, varnames: Optional[amr.Vector_string], - geom: Optional[amr.Geometry]) -> np.ndarray: + def postprocess(self, multifab: amr.MultiFab, + varnames: Optional[amr.Vector_string] = None, + geom: Optional[amr.Geometry] = None) -> np.ndarray: """ - Heat equation specific postprocessing with geometry awareness. - + Extract output quantities from simulation results. + Parameters: ----------- multifab : amr.MultiFab - Simulation data + Simulation data (temperature field) varnames : amr.Vector_string or None Variable names geom : amr.Geometry or None Domain geometry - + Returns: -------- np.ndarray - Processed outputs [max, mean, std, integral/sum, center_value] + Output quantities [max, mean, std, total_energy, center_value] """ xp = load_cupy() - + # Get basic statistics max_val = multifab.max(comp=0, local=False) sum_val = multifab.sum(comp=0, local=False) total_cells = multifab.box_array().numPts mean_val = sum_val / total_cells - - # Calculate standard deviation + + # Calculate standard deviation using L2 norm l2_norm = multifab.norm2(0) sum_sq = l2_norm**2 variance = (sum_sq / total_cells) - mean_val**2 std_val = np.sqrt(max(0, variance)) - - # Get value at center (if geometry available) + + # Calculate total energy (normalized by cell volume if geometry available) + if geom is not None: + dx = geom.data().CellSize() + cell_volume = dx[0] * dx[1] * dx[2] + total_energy = sum_val * cell_volume * self.density * self.specific_heat + else: + total_energy = sum_val # Dimensionless if no geometry + + # Get temperature at domain center center_val = 0.0 if geom is not None: dx = geom.data().CellSize() prob_lo = geom.data().ProbLo() prob_hi = geom.data().ProbHi() - + # Calculate center coordinates center_coords = [(prob_lo[i] + prob_hi[i]) / 2.0 for i in range(3)] - + # Find the cell index closest to center - center_indices = [] - for i in range(3): - idx = int((center_coords[i] - prob_lo[i]) / dx[i]) - center_indices.append(idx) - - # Get value at center (default to 0 if can't access) + center_indices = [int((center_coords[i] - prob_lo[i]) / dx[i]) for i in range(3)] + + # Get value at center try: for mfi in multifab: bx = mfi.validbox() - if (center_indices[0] >= bx.small_end[0] and center_indices[0] <= bx.big_end[0] and - center_indices[1] >= bx.small_end[1] and center_indices[1] <= bx.big_end[1] and - center_indices[2] >= bx.small_end[2] and center_indices[2] <= bx.big_end[2]): - + if all(center_indices[i] >= bx.small_end[i] and + center_indices[i] <= bx.big_end[i] for i in range(3)): + state_arr = xp.array(multifab.array(mfi), copy=False) # Convert global to local indices - local_i = center_indices[0] - bx.small_end[0] - local_j = center_indices[1] - bx.small_end[1] - local_k = center_indices[2] - bx.small_end[2] - center_val = float(state_arr[0, local_k, local_j, local_i]) + local_indices = [center_indices[i] - bx.small_end[i] for i in range(3)] + center_val = float(state_arr[0, local_indices[2], + local_indices[1], local_indices[0]]) break - except (IndexError, AttributeError): - # Fall back to (0,0,0) if center calculation fails - for mfi in multifab: - state_arr = xp.array(multifab.array(mfi), copy=False) - center_val = float(state_arr[0, 0, 0, 0]) - break + except (IndexError, AttributeError) as e: + print(f"Warning: Could not access center cell: {e}") + center_val = mean_val # Fall back to mean else: - # No geometry available, use (0,0,0,0) cell - try: - for mfi in multifab: - state_arr = xp.array(multifab.array(mfi), copy=False) - center_val = float(state_arr[0, 0, 0, 0]) - break - except (IndexError, AttributeError): - amr.Print("Warning: Could not access cell (0,0,0,0)") - - return np.array([max_val, mean_val, std_val, sum_val, center_val]) + center_val = mean_val # No geometry, use mean as proxy - def get_pnames(self) -> List[str]: - return ["diffusion_coefficient", "initial_amplitude", "initial_width"] + return np.array([max_val, mean_val, std_val, total_energy, center_val]) - def get_outnames(self) -> List[str]: - return ["max", "mean", "std", "integral", "center"] + def validate_outputs(self, outputs: np.ndarray) -> bool: + """ + Validate that outputs are physically reasonable. -if __name__ == "__main__": + Parameters: + ----------- + outputs : np.ndarray + Output values to validate + Returns: + -------- + bool + True if outputs are valid + """ + if outputs.shape[-1] != len(self.output_names): + return False + + # Check each output against its valid range + for i, output_name in enumerate(self.output_names): + info = self.get_output_info(output_name) + if 'valid_range' in info: + min_val, max_val = info['valid_range'] + if outputs[..., i].min() < min_val or outputs[..., i].max() > max_val: + print(f"Warning: {output_name} outside valid range {info['valid_range']}") + return False + + # Physical consistency checks + max_temp = outputs[..., 0] + mean_temp = outputs[..., 1] + + # Maximum should be >= mean + if np.any(max_temp < mean_temp): + print("Warning: Maximum temperature less than mean") + return False + + return True + + +if __name__ == "__main__": # Initialize AMReX amr.initialize([]) - # Create model using ParmParse to read from inputs file - model = HeatEquationModel(use_parmparse=True) + # Create model with default parameters + model = HeatEquationModel(n_cell=32, nsteps=100, dt=1e-5) + + print("Heat Equation Model Configuration:") + print("=" * 50) + print(f"Parameter fields: {model.param_names}") + print(f"Output fields: {model.output_names}") + print(f"Domain: {model.domain_left_edge} to {model.domain_right_edge}") + print(f"Grid: {model.domain_dimensions}") - print(f"Heat equation model initialized with:") - print(f" n_cell = {model.n_cell}") - print(f" max_grid_size = {model.max_grid_size}") - print(f" nsteps = {model.nsteps}") - print(f" plot_int = {model.plot_int}") - print(f" dt = {model.dt}") + # Show statistical properties + print("\nParameter Statistics:") + print("-" * 50) + for i, param_name in enumerate(model.param_names): + info = model.get_param_info(param_name) + print(f"{param_name}:") + print(f" Mean: {model.modelpar['mean'][i]:.3f} {info.get('units', '')}") + print(f" Std: {model.modelpar['std'][i]:.3f}") + print(f" Bounds: {info.get('bounds', 'Not specified')}") + print(f" Distribution: {info.get('distribution', 'normal')}") - # Test with random parameters + # Test with different parameter sets test_params = np.array([ [1.0, 1.0, 0.01], # baseline [2.0, 1.5, 0.02], # higher diffusion, higher amplitude [0.5, 2.0, 0.005] # lower diffusion, higher amplitude, narrower ]) - print("\nRunning heat equation with parameters:") - print(" [diffusion, amplitude, width]") - print(test_params) - + print("\nRunning simulations...") + print("-" * 50) outputs = model(test_params) - print("\nResults [max, mean, std, integral, center]:") - print(outputs) + print("\nResults:") + print("-" * 50) + for i, params in enumerate(test_params): + print(f"Run {i+1}: D={params[0]:.2f}, A={params[1]:.2f}, W={params[2]:.3f}") + for j, output_name in enumerate(model.output_names): + info = model.get_output_info(output_name) + units = info.get('units', '') + print(f" {info.get('display_name', output_name)}: {outputs[i,j]:.4f} {units}") + + # Validate outputs + if model.validate_outputs(outputs): + print("\n✓ All outputs are physically valid") + else: + print("\n✗ Some outputs failed validation") + + # Write parameter marginals for UQ analysis + model.write_param_marginals('heat_equation_marginals.txt') # Finalize AMReX amr.finalize() diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py index a6b044b3..03533917 100755 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py @@ -34,14 +34,14 @@ def load_cupy(): return np -def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, - plot_int: int = 100, dt: float = 1e-5, plot_files_output: bool = False, - verbose: int = 1, diffusion_coeff: float = 1.0, init_amplitude: float = 1.0, +def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, + plot_int: int = 100, dt: float = 1e-5, plot_files_output: bool = False, + verbose: int = 1, diffusion_coeff: float = 1.0, init_amplitude: float = 1.0, init_width: float = 0.01) -> Tuple[amr.MultiFab, amr.Geometry]: """ Run the heat equation simulation. The main function, automatically called below if called as a script. - + Returns: -------- tuple : (phi_new, geom) @@ -192,11 +192,11 @@ def parse_inputs() -> Dict[str, Union[int, float, bool]]: 'init_amplitude': 1.0, 'init_width': 0.01 } - + try: # Convert entire ParmParse table to Python dictionary all_params = pp.to_dict() - + # Extract our specific parameters with proper type conversion params = {} for key, default_value in defaults.items(): @@ -224,14 +224,14 @@ def parse_inputs() -> Dict[str, Union[int, float, bool]]: params[key] = default_value else: params[key] = default_value - + # Optional: print the parameters we're actually using amr.Print("Using parameters:") for key, value in params.items(): amr.Print(f" {key}: {value}") - + return params - + except Exception as e: amr.Print(f"Warning: Could not parse parameters with to_dict(): {e}") amr.Print("Using default values") @@ -244,10 +244,10 @@ def parse_inputs() -> Dict[str, Union[int, float, bool]]: try: # Parse inputs params = parse_inputs() - + # Run simulation main(**params) - + finally: # Finalize AMReX amr.finalize() From 79394253be832298c2e93c74d8222c25bb758553 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 11:35:07 -0700 Subject: [PATCH 051/142] mv --- .../Case-2/{BaseModel.py => AMReXBaseModel.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename GuidedTutorials/HeatEquation_PythonDriver/Case-2/{BaseModel.py => AMReXBaseModel.py} (100%) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXBaseModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-2/BaseModel.py rename to GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXBaseModel.py From b50fc4202d8355464dc350333d2ab9c81b95fad3 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 14:54:07 -0700 Subject: [PATCH 052/142] Initial files --- .../Case-0/AMReXBaseModel.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py new file mode 100644 index 00000000..6593a0fb --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -0,0 +1,22 @@ +from pytuq.func.func import ModelWrapperFcn +import numpy as np +from abc import abstractmethod + +from pytuq.func.func import ModelWrapperFcn +import numpy as np + +class AMReXBaseModel(ModelWrapperFcn): + """Base class for AMReX models with yt-style field info""" + + # Class-level field definitions (to be overridden by subclasses) + _field_info_class = None + _param_fields = [] + _output_fields = [] + _spatial_domain_bounds = None + + def __init__(self, **kwargs): + """Minimal initialization for now""" + # Just call parent for now + super().__init__(lambda x: x, ndim=1, **kwargs) + print("✓ Basic initialization works") + From 2c12e4d4896a30b8bdb14c24fba56368112a0e6d Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 14:58:53 -0700 Subject: [PATCH 053/142] Continue adding fields --- .../Case-0/AMReXBaseModel.py | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index 6593a0fb..fae20b2a 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -15,8 +15,32 @@ class AMReXBaseModel(ModelWrapperFcn): _spatial_domain_bounds = None def __init__(self, **kwargs): - """Minimal initialization for now""" - # Just call parent for now - super().__init__(lambda x: x, ndim=1, **kwargs) - print("✓ Basic initialization works") + + # Create field info + self.field_info = self._create_field_info() + print(f"✓ Created field_info with {len(self.field_info)} fields") + def _create_field_info(self): + """Create yt-style field info container""" + field_info = {} + + for field_tuple in self._param_fields: + field_info[field_tuple] = self._get_field_info(field_tuple) + + for field_tuple in self._output_fields: + field_info[field_tuple] = self._get_field_info(field_tuple) + + return field_info + + def _get_field_info(self, field_tuple): + """ + Override in subclass to provide field metadata. + + Args: + field_tuple: (field_type, field_name) tuple + + Returns: + dict with 'bounds', 'units', 'mean', 'std', etc. + """ + # Default empty implementation + return {} From b471f3b14de39a19ccdbceb738b8f80db4761d83 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 15:01:22 -0700 Subject: [PATCH 054/142] add some parameter domain --- .../Case-0/AMReXBaseModel.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index fae20b2a..220fb4d5 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -32,6 +32,23 @@ def _create_field_info(self): return field_info + def _extract_param_domain(self): + """Extract parameter bounds into Function-compatible domain array""" + domain_list = [] + + for field_tuple in self._param_fields: + info = self._get_field_info(field_tuple) + if 'bounds' in info: + domain_list.append(info['bounds']) + else: + # Use Function's default domain size + dmax = getattr(self, 'dmax', 10.0) + domain_list.append([-dmax, dmax]) + + if domain_list: + return np.array(domain_list) + return None + def _get_field_info(self, field_tuple): """ Override in subclass to provide field metadata. From afdd3d6d4b4550809f668083abaeb92aac9d52c1 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 15:03:50 -0700 Subject: [PATCH 055/142] Basic params working --- .../Case-0/AMReXBaseModel.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index 220fb4d5..f6c6a6a8 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -15,10 +15,18 @@ class AMReXBaseModel(ModelWrapperFcn): _spatial_domain_bounds = None def __init__(self, **kwargs): + # Create modelpar + modelpar = self._create_modelpar() + + super().__init__(lambda x: x, + ndim=len(modelpar['param_names']) or 1, + modelpar=modelpar, + **kwargs) - # Create field info self.field_info = self._create_field_info() - print(f"✓ Created field_info with {len(self.field_info)} fields") + self.param_names = modelpar['param_names'] + self.output_names = modelpar['output_names'] + print(f"✓ Params: {self.param_names}, Outputs: {self.output_names}") def _create_field_info(self): """Create yt-style field info container""" @@ -61,3 +69,11 @@ def _get_field_info(self, field_tuple): """ # Default empty implementation return {} + + def _create_modelpar(self): + """Create basic modelpar dictionary""" + modelpar = { + 'param_names': [f[1] for f in self._param_fields], + 'output_names': [f[1] for f in self._output_fields], + } + return modelpar From 36058d9d0254d51925aa88a4f61026842d6364ff Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 15:05:35 -0700 Subject: [PATCH 056/142] Default run method --- .../Case-0/AMReXBaseModel.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index f6c6a6a8..66fb724e 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -70,6 +70,36 @@ def _get_field_info(self, field_tuple): # Default empty implementation return {} + def _run_simulation(self, params): + """ + Core simulation logic - override in subclasses or use evolve/postprocess. + + Args: + params: numpy array of parameters (1D or 2D) + + Returns: + numpy array of outputs + """ + # Ensure params is 2D (n_samples x n_params) + if params.ndim == 1: + params = params.reshape(1, -1) + + n_samples = params.shape[0] + outdim = len(self.output_names) if self.output_names else 1 + outputs = np.zeros((n_samples, outdim)) + + # Check if subclass has evolve/postprocess methods + if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): + for i in range(n_samples): + multifab, varnames, geom = self.evolve(params[i, :]) + outputs[i, :] = self.postprocess(multifab, varnames, geom) + else: + raise NotImplementedError( + "Must implement _run_simulation or evolve/postprocess methods" + ) + + return outputs + def _create_modelpar(self): """Create basic modelpar dictionary""" modelpar = { From 4c13ed87ebed939d4b8010496db20396a468a8c4 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 15:12:20 -0700 Subject: [PATCH 057/142] Make dimension fixes, don't require tuple returned from evolve --- .../Case-0/AMReXBaseModel.py | 61 ++++++++++++++++--- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index 66fb724e..f3928281 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -57,18 +57,44 @@ def _extract_param_domain(self): return np.array(domain_list) return None - def _get_field_info(self, field_tuple): + def forward(self, x): """ - Override in subclass to provide field metadata. - + PyTorch-compatible forward method for inference. + Args: - field_tuple: (field_type, field_name) tuple - + x: torch.Tensor or np.ndarray Returns: - dict with 'bounds', 'units', 'mean', 'std', etc. + torch.Tensor if input is tensor, else np.ndarray """ - # Default empty implementation - return {} + # Check if input is PyTorch tensor + is_torch = False + if hasattr(x, 'detach'): # Duck typing for torch.Tensor + is_torch = True + import torch + x_np = x.detach().cpu().numpy() + else: + x_np = x + + # Run simulation using existing logic + outputs = self._run_simulation(x_np) + + # Convert back to torch if needed + if is_torch: + return torch.from_numpy(outputs).to(x.device) + return outputs + + def __call__(self, x): + """Function interface - routes through forward() for consistency""" + # Ensure x is at least 2D for checkDim + x = np.asarray(x) + if x.ndim == 1: + x = x.reshape(1, -1) + + self.checkDim(x) + if hasattr(self, 'domain') and self.domain is not None: + self.checkDomain(x) + + return self.forward(x) def _run_simulation(self, params): """ @@ -91,8 +117,10 @@ def _run_simulation(self, params): # Check if subclass has evolve/postprocess methods if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): for i in range(n_samples): - multifab, varnames, geom = self.evolve(params[i, :]) - outputs[i, :] = self.postprocess(multifab, varnames, geom) + # Evolve just returns simulation state + sim_state = self.evolve(params[i, :]) + # Postprocess extracts outputs from state + outputs[i, :] = self.postprocess(sim_state) else: raise NotImplementedError( "Must implement _run_simulation or evolve/postprocess methods" @@ -100,6 +128,19 @@ def _run_simulation(self, params): return outputs + def _get_field_info(self, field_tuple): + """ + Override in subclass to provide field metadata. + + Args: + field_tuple: (field_type, field_name) tuple + + Returns: + dict with 'bounds', 'units', 'mean', 'std', etc. + """ + # Default empty implementation + return {} + def _create_modelpar(self): """Create basic modelpar dictionary""" modelpar = { From 842c8433d8c65df07d70da7024cbefe93eb1330d Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 15:16:00 -0700 Subject: [PATCH 058/142] Helper functions --- .../Case-0/AMReXBaseModel.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index f3928281..c7c9fe00 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -128,6 +128,11 @@ def _run_simulation(self, params): return outputs + @property + def field_list(self): + """All available fields""" + return list(self.field_info.keys()) + def _get_field_info(self, field_tuple): """ Override in subclass to provide field metadata. @@ -141,6 +146,20 @@ def _get_field_info(self, field_tuple): # Default empty implementation return {} + def get_param_info(self, param_name): + """Get info for a specific parameter""" + for field_tuple in self._param_fields: + if field_tuple[1] == param_name: + return self._get_field_info(field_tuple) + return {} + + def get_output_info(self, output_name): + """Get info for a specific output""" + for field_tuple in self._output_fields: + if field_tuple[1] == output_name: + return self._get_field_info(field_tuple) + return {} + def _create_modelpar(self): """Create basic modelpar dictionary""" modelpar = { From 8576890ae665b859fa5373fc6c9876afac766633 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 15:18:33 -0700 Subject: [PATCH 059/142] Add spatial tracking --- .../Case-0/AMReXBaseModel.py | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index c7c9fe00..7050a074 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -7,7 +7,7 @@ class AMReXBaseModel(ModelWrapperFcn): """Base class for AMReX models with yt-style field info""" - + # Class-level field definitions (to be overridden by subclasses) _field_info_class = None _param_fields = [] @@ -17,16 +17,34 @@ class AMReXBaseModel(ModelWrapperFcn): def __init__(self, **kwargs): # Create modelpar modelpar = self._create_modelpar() - - super().__init__(lambda x: x, + + # Determine dimensions + ndim = len(modelpar['param_names']) or 1 + outdim = len(modelpar['output_names']) or 1 + + super().__init__(lambda x: x, ndim=len(modelpar['param_names']) or 1, modelpar=modelpar, **kwargs) - + + # Setup field info and names self.field_info = self._create_field_info() self.param_names = modelpar['param_names'] self.output_names = modelpar['output_names'] - print(f"✓ Params: {self.param_names}, Outputs: {self.output_names}") + self.outdim = outdim + + # Extract and set parameter domain + param_domain = self._extract_param_domain() + if param_domain is not None and len(param_domain) > 0: + self.setDimDom(domain=param_domain) + + # Setup spatial domain bounds (yt-style) - separate from parameter bounds + if self._spatial_domain_bounds: + self.domain_left_edge = self._spatial_domain_bounds[0] + self.domain_right_edge = self._spatial_domain_bounds[1] + self.domain_dimensions = (self._spatial_domain_bounds[2] + if len(self._spatial_domain_bounds) > 2 + else None) def _create_field_info(self): """Create yt-style field info container""" @@ -89,31 +107,31 @@ def __call__(self, x): x = np.asarray(x) if x.ndim == 1: x = x.reshape(1, -1) - + self.checkDim(x) if hasattr(self, 'domain') and self.domain is not None: self.checkDomain(x) - + return self.forward(x) def _run_simulation(self, params): """ Core simulation logic - override in subclasses or use evolve/postprocess. - + Args: params: numpy array of parameters (1D or 2D) - + Returns: numpy array of outputs """ # Ensure params is 2D (n_samples x n_params) if params.ndim == 1: params = params.reshape(1, -1) - + n_samples = params.shape[0] outdim = len(self.output_names) if self.output_names else 1 outputs = np.zeros((n_samples, outdim)) - + # Check if subclass has evolve/postprocess methods if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): for i in range(n_samples): @@ -125,7 +143,7 @@ def _run_simulation(self, params): raise NotImplementedError( "Must implement _run_simulation or evolve/postprocess methods" ) - + return outputs @property @@ -136,10 +154,10 @@ def field_list(self): def _get_field_info(self, field_tuple): """ Override in subclass to provide field metadata. - + Args: field_tuple: (field_type, field_name) tuple - + Returns: dict with 'bounds', 'units', 'mean', 'std', etc. """ From c3c37397f5564f4db08a63f8948dc0a35c4cac35 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 15:20:54 -0700 Subject: [PATCH 060/142] Add marg and more parameter fields --- .../Case-0/AMReXBaseModel.py | 76 ++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index 7050a074..ff810361 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -179,9 +179,79 @@ def get_output_info(self, output_name): return {} def _create_modelpar(self): - """Create basic modelpar dictionary""" + """Create modelpar dictionary with statistical properties""" modelpar = { - 'param_names': [f[1] for f in self._param_fields], - 'output_names': [f[1] for f in self._output_fields], + 'param_info': {}, + 'output_info': {}, + 'param_names': [], + 'output_names': [], + 'defaults': {}, + 'bounds': {}, + 'units': {}, + 'mean': [], + 'std': [], + 'distribution': [], # 'normal', 'uniform', 'lognormal', etc. + 'pc_type': 'HG', # Hermite-Gaussian by default } + + # Extract parameter information including statistical properties + for field_tuple in self._param_fields: + field_type, field_name = field_tuple + info = self._get_field_info(field_tuple) + + modelpar['param_info'][field_name] = info + modelpar['param_names'].append(field_name) + + # Extract statistical properties + if 'mean' in info: + modelpar['mean'].append(info['mean']) + elif 'default' in info: + modelpar['mean'].append(info['default']) + else: + # Use center of bounds if available + if 'bounds' in info: + modelpar['mean'].append(np.mean(info['bounds'])) + else: + modelpar['mean'].append(0.0) + + if 'std' in info: + modelpar['std'].append(info['std']) + else: + # Default to 10% of mean or range + if 'bounds' in info: + # Use range/6 as rough std (99.7% within bounds) + modelpar['std'].append((info['bounds'][1] - info['bounds'][0])/6.0) + else: + modelpar['std'].append(abs(modelpar['mean'][-1]) * 0.1) + + # Store other properties + if 'bounds' in info: + modelpar['bounds'][field_name] = info['bounds'] + if 'units' in info: + modelpar['units'][field_name] = info['units'] + if 'distribution' in info: + modelpar['distribution'].append(info['distribution']) + else: + modelpar['distribution'].append('normal') # default + + # Convert to numpy arrays for easier manipulation + modelpar['mean'] = np.array(modelpar['mean']) + modelpar['std'] = np.array(modelpar['std']) + + # Add output information + for field_tuple in self._output_fields: + field_type, field_name = field_tuple + info = self._get_field_info(field_tuple) + modelpar['output_info'][field_name] = info + modelpar['output_names'].append(field_name) + return modelpar + + def write_param_marginals(self, filename='param_margpc.txt'): + """Write parameter marginals file for PC analysis""" + with open(filename, 'w') as f: + for i, name in enumerate(self.modelpar['param_names']): + mean = self.modelpar['mean'][i] + std = self.modelpar['std'][i] + f.write(f"{mean} {std}\n") + print(f"Wrote parameter marginals to {filename}") From ddc188990271d45c96cbf59ad2036b2993e02241 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 15:45:29 -0700 Subject: [PATCH 061/142] Update HeatEquationModel inheritance --- .../Case-0/AMReXBaseModel.py | 25 ++ .../Case-0/HeatEquationModel.py | 288 ++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index ff810361..b8c4036f 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -4,6 +4,27 @@ from pytuq.func.func import ModelWrapperFcn import numpy as np +import amrex.space3d as amr + +def load_cupy(): + """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" + if amr.Config.have_gpu: + try: + import cupy as cp + amr.Print("Note: found and will use cupy") + return cp + except ImportError: + amr.Print("Warning: GPU found but cupy not available! Using numpy...") + import numpy as np + return np + if amr.Config.gpu_backend == "SYCL": + amr.Print("Warning: SYCL GPU backend not yet implemented for Python") + import numpy as np + return np + else: + import numpy as np + amr.Print("Note: found and will use numpy") + return np class AMReXBaseModel(ModelWrapperFcn): """Base class for AMReX models with yt-style field info""" @@ -15,6 +36,10 @@ class AMReXBaseModel(ModelWrapperFcn): _spatial_domain_bounds = None def __init__(self, **kwargs): + """Initialize AMReX if needed.""" + if not amr.initialized(): + amr.initialize([]) + # Create modelpar modelpar = self._create_modelpar() diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py new file mode 100644 index 00000000..7c1211e9 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python3 +import numpy as np +from typing import Tuple, List, Optional +import amrex.space3d as amr +from AMReXModelBase import AMReXModelBase, load_cupy + + +class HeatEquationModel(AMReXModelBase): + """ + Heat equation simulation model with full statistical properties + and AMReX integration. + """ + + # Define parameter fields with proper naming convention + _param_fields = [ + ('param', 'diffusion_coefficient'), + ('param', 'initial_amplitude'), + ('param', 'initial_width'), + ] + + # Define output fields + _output_fields = [ + ('output', 'max_temperature'), + ('output', 'mean_temperature'), + ('output', 'std_temperature'), + ('output', 'total_energy'), + ('output', 'center_temperature'), + ] + + # Spatial domain bounds (3D heat equation domain) + _spatial_domain_bounds = [ + np.array([0.0, 0.0, 0.0]), # left edge + np.array([1.0, 1.0, 1.0]), # right edge + np.array([32, 32, 32]) # default grid dimensions + ] + + def __init__(self, n_cell: int = 32, max_grid_size: int = 16, + nsteps: int = 1000, plot_int: int = 100, + dt: float = 1e-5, use_parmparse: bool = False, **kwargs): + """ + Initialize heat equation model. + + Parameters: + ----------- + n_cell : int + Number of grid cells in each dimension + max_grid_size : int + Maximum grid size for AMR + nsteps : int + Number of time steps + plot_int : int + Plot interval (for output control) + dt : float + Time step size + use_parmparse : bool + Whether to use AMReX ParmParse for input + """ + # Store simulation parameters + if use_parmparse: + from main import parse_inputs + params = parse_inputs() + self.n_cell = params['n_cell'] + self.max_grid_size = params['max_grid_size'] + self.nsteps = params['nsteps'] + self.plot_int = params['plot_int'] + self.dt = params['dt'] + else: + self.n_cell = n_cell + self.max_grid_size = max_grid_size + self.nsteps = nsteps + self.plot_int = plot_int + self.dt = dt + + # Update spatial domain dimensions based on n_cell + if self.n_cell != 32: + self._spatial_domain_bounds[2] = np.array([self.n_cell, self.n_cell, self.n_cell]) + + # Initialize base class + super().__init__(**kwargs) + + def _get_field_info(self, field_tuple): + """ + Provide field metadata with statistical properties. + Only includes fields that are actually used by the framework. + """ + field_type, field_name = field_tuple + + field_info_dict = { + # Parameters - match param_margpc.txt values + ('param', 'diffusion_coefficient'): { + 'mean': 1.0, + 'std': 0.25, + 'bounds': [0.1, 5.0], + 'distribution': 'normal', + 'units': 'm**2/s', + 'display_name': 'Thermal Diffusivity', + }, + ('param', 'initial_amplitude'): { + 'mean': 1.0, + 'std': 0.25, + 'bounds': [0.1, 3.0], + 'distribution': 'normal', + 'units': 'K', + 'display_name': 'Initial Temperature', + }, + ('param', 'initial_width'): { + 'mean': 0.01, + 'std': 0.0025, + 'bounds': [0.001, 0.1], + 'distribution': 'normal', + 'units': 'm', + 'display_name': 'Initial Width', + }, + + # Outputs - just units and display names + ('output', 'max_temperature'): { + 'units': 'K', + 'display_name': 'Maximum Temperature', + }, + ('output', 'mean_temperature'): { + 'units': 'K', + 'display_name': 'Mean Temperature', + }, + ('output', 'std_temperature'): { + 'units': 'K', + 'display_name': 'Temperature Std Dev', + }, + ('output', 'total_energy'): { + 'units': 'J', + 'display_name': 'Total Energy', + }, + ('output', 'center_temperature'): { + 'units': 'K', + 'display_name': 'Center Temperature', + }, + } + + return field_info_dict.get(field_tuple, {}) + + def evolve(self, param_set: np.ndarray) + """ + Run heat equation simulation. + + Parameters: + ----------- + param_set : np.ndarray + [diffusion_coeff, init_amplitude, init_width] + + Returns: + -------- + tuple : (phi_new, varnames, geom) + Ready to pass to write_single_level_plotfile + """ + from main import main # Import here to avoid circular imports + + if len(param_set) != 3: + raise ValueError(f"Expected 3 parameters, got {len(param_set)}") + + phi_new, geom = main( + diffusion_coeff=float(param_set[0]), + init_amplitude=float(param_set[1]), + init_width=float(param_set[2]), + n_cell=self.n_cell, + max_grid_size=self.max_grid_size, + nsteps=self.nsteps, + plot_int=self.plot_int, + dt=self.dt, + plot_files_output=False, + verbose=0 + ) + + varnames = amr.Vector_string(['phi']) + return phi_new, varnames, geom + + def postprocess(self, sim_state) -> np.ndarray: + """ + Heat equation specific postprocessing with geometry awareness. + + Parameters: + ----------- + multifab : amr.MultiFab + Simulation data + varnames : amr.Vector_string or None + Variable names + geom : amr.Geometry or None + Domain geometry + + Returns: + -------- + np.ndarray + Processed outputs [max, mean, std, integral/sum, center_value] + """ + xp = load_cupy() + + # Get basic statistics + max_val = multifab.max(comp=0, local=False) + sum_val = multifab.sum(comp=0, local=False) + total_cells = multifab.box_array().numPts + mean_val = sum_val / total_cells + + # Calculate standard deviation + l2_norm = multifab.norm2(0) + sum_sq = l2_norm**2 + variance = (sum_sq / total_cells) - mean_val**2 + std_val = np.sqrt(max(0, variance)) + + # Get value at center (if geometry available) + center_val = 0.0 + if geom is not None: + dx = geom.data().CellSize() + prob_lo = geom.data().ProbLo() + prob_hi = geom.data().ProbHi() + + # Calculate center coordinates + center_coords = [(prob_lo[i] + prob_hi[i]) / 2.0 for i in range(3)] + + # Find the cell index closest to center + center_indices = [] + for i in range(3): + idx = int((center_coords[i] - prob_lo[i]) / dx[i]) + center_indices.append(idx) + + # Get value at center (default to 0 if can't access) + try: + for mfi in multifab: + bx = mfi.validbox() + if (center_indices[0] >= bx.small_end[0] and center_indices[0] <= bx.big_end[0] and + center_indices[1] >= bx.small_end[1] and center_indices[1] <= bx.big_end[1] and + center_indices[2] >= bx.small_end[2] and center_indices[2] <= bx.big_end[2]): + + state_arr = xp.array(multifab.array(mfi), copy=False) + # Convert global to local indices + local_i = center_indices[0] - bx.small_end[0] + local_j = center_indices[1] - bx.small_end[1] + local_k = center_indices[2] - bx.small_end[2] + center_val = float(state_arr[0, local_k, local_j, local_i]) + break + except (IndexError, AttributeError): + # Fall back to (0,0,0) if center calculation fails + for mfi in multifab: + state_arr = xp.array(multifab.array(mfi), copy=False) + center_val = float(state_arr[0, 0, 0, 0]) + break + else: + # No geometry available, use (0,0,0,0) cell + try: + for mfi in multifab: + state_arr = xp.array(multifab.array(mfi), copy=False) + center_val = float(state_arr[0, 0, 0, 0]) + break + except (IndexError, AttributeError): + amr.Print("Warning: Could not access cell (0,0,0,0)") + + return np.array([max_val, mean_val, std_val, sum_val, center_val]) + +if __name__ == "__main__": + + # Initialize AMReX + amr.initialize([]) + + # Create model using ParmParse to read from inputs file + model = HeatEquationModel(use_parmparse=True) + + print(f"Heat equation model initialized with:") + print(f" n_cell = {model.n_cell}") + print(f" max_grid_size = {model.max_grid_size}") + print(f" nsteps = {model.nsteps}") + print(f" plot_int = {model.plot_int}") + print(f" dt = {model.dt}") + + # Test with random parameters + test_params = np.array([ + [1.0, 1.0, 0.01], # baseline + [2.0, 1.5, 0.02], # higher diffusion, higher amplitude + [0.5, 2.0, 0.005] # lower diffusion, higher amplitude, narrower + ]) + + print("\nRunning heat equation with parameters:") + print(" [diffusion, amplitude, width]") + print(test_params) + + outputs = model(test_params) + + print("\nResults [max, mean, std, integral, center]:") + print(outputs) + + # Finalize AMReX + amr.finalize() From 13309cb60608c30805bbcea0d1e4230dc47cb008 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 16:22:34 -0700 Subject: [PATCH 062/142] Switch around eval and make sure we're returning things --- .../Case-0/AMReXBaseModel.py | 70 ++++++++++++------- .../Case-0/HeatEquationModel.py | 49 +++++++------ 2 files changed, 72 insertions(+), 47 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index b8c4036f..909628a9 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -35,31 +35,42 @@ class AMReXBaseModel(ModelWrapperFcn): _output_fields = [] _spatial_domain_bounds = None - def __init__(self, **kwargs): - """Initialize AMReX if needed.""" - if not amr.initialized(): - amr.initialize([]) - - # Create modelpar + def __init__(self, model=None, **kwargs): + # Create modelpar from existing parameter information modelpar = self._create_modelpar() + # Setup field info container + self.field_info = self._create_field_info() + + # Setup convenience lists + self.param_names = [f[1] for f in self._param_fields] + self.output_names = [f[1] for f in self._output_fields] + + # Extract parameter bounds and create domain array for Function + param_domain = self._extract_param_domain() + # Determine dimensions - ndim = len(modelpar['param_names']) or 1 - outdim = len(modelpar['output_names']) or 1 + ndim = len(self.param_names) + outdim = len(self.output_names) - super().__init__(lambda x: x, - ndim=len(modelpar['param_names']) or 1, - modelpar=modelpar, - **kwargs) + # Create model function wrapper + if model is None: + model_func = lambda params, mp=None: self._run_simulation(params) + else: + model_func = model - # Setup field info and names - self.field_info = self._create_field_info() - self.param_names = modelpar['param_names'] - self.output_names = modelpar['output_names'] + # Initialize Function with model + super().__init__( + model_func, + ndim, + modelpar=modelpar, + name=kwargs.get('name', 'AMReXModel') + ) + + # Set output dimension (ModelWrapperFcn defaults to 1) self.outdim = outdim - # Extract and set parameter domain - param_domain = self._extract_param_domain() + # Set the parameter domain using Function's method if param_domain is not None and len(param_domain) > 0: self.setDimDom(domain=param_domain) @@ -71,6 +82,11 @@ def __init__(self, **kwargs): if len(self._spatial_domain_bounds) > 2 else None) + # Initialize AMReX if needed + self.xp = load_cupy() + if not amr.initialized(): + amr.initialize([]) + def _create_field_info(self): """Create yt-style field info container""" field_info = {} @@ -127,17 +143,19 @@ def forward(self, x): return outputs def __call__(self, x): - """Function interface - routes through forward() for consistency""" - # Ensure x is at least 2D for checkDim - x = np.asarray(x) - if x.ndim == 1: - x = x.reshape(1, -1) - self.checkDim(x) if hasattr(self, 'domain') and self.domain is not None: - self.checkDomain(x) + self.checkDomain(x) # ← Added domain checking + + if self.modelpar is None: + outputs = self.model(x) + else: + outputs = self.model(x, self.modelpar) - return self.forward(x) + if outputs.ndim == 1: + outputs = outputs.reshape(-1, self.outdim) # ← Allows MULTIPLE outputs + + return outputs def _run_simulation(self, params): """ diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py index 7c1211e9..620902d3 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py @@ -2,10 +2,10 @@ import numpy as np from typing import Tuple, List, Optional import amrex.space3d as amr -from AMReXModelBase import AMReXModelBase, load_cupy +from AMReXBaseModel import AMReXBaseModel, load_cupy -class HeatEquationModel(AMReXModelBase): +class HeatEquationModel(AMReXBaseModel): """ Heat equation simulation model with full statistical properties and AMReX integration. @@ -111,7 +111,7 @@ def _get_field_info(self, field_tuple): 'units': 'm', 'display_name': 'Initial Width', }, - + # Outputs - just units and display names ('output', 'max_temperature'): { 'units': 'K', @@ -137,25 +137,25 @@ def _get_field_info(self, field_tuple): return field_info_dict.get(field_tuple, {}) - def evolve(self, param_set: np.ndarray) + def evolve(self, param_set: np.ndarray): """ Run heat equation simulation. - + Parameters: ----------- param_set : np.ndarray [diffusion_coeff, init_amplitude, init_width] - + Returns: -------- tuple : (phi_new, varnames, geom) Ready to pass to write_single_level_plotfile """ from main import main # Import here to avoid circular imports - + if len(param_set) != 3: raise ValueError(f"Expected 3 parameters, got {len(param_set)}") - + phi_new, geom = main( diffusion_coeff=float(param_set[0]), init_amplitude=float(param_set[1]), @@ -168,14 +168,14 @@ def evolve(self, param_set: np.ndarray) plot_files_output=False, verbose=0 ) - + varnames = amr.Vector_string(['phi']) return phi_new, varnames, geom def postprocess(self, sim_state) -> np.ndarray: """ Heat equation specific postprocessing with geometry awareness. - + Parameters: ----------- multifab : amr.MultiFab @@ -184,42 +184,43 @@ def postprocess(self, sim_state) -> np.ndarray: Variable names geom : amr.Geometry or None Domain geometry - + Returns: -------- np.ndarray Processed outputs [max, mean, std, integral/sum, center_value] """ xp = load_cupy() - + [multifab, varnames, geom] = sim_state + # Get basic statistics max_val = multifab.max(comp=0, local=False) sum_val = multifab.sum(comp=0, local=False) total_cells = multifab.box_array().numPts mean_val = sum_val / total_cells - + # Calculate standard deviation l2_norm = multifab.norm2(0) sum_sq = l2_norm**2 variance = (sum_sq / total_cells) - mean_val**2 std_val = np.sqrt(max(0, variance)) - + # Get value at center (if geometry available) center_val = 0.0 if geom is not None: dx = geom.data().CellSize() prob_lo = geom.data().ProbLo() prob_hi = geom.data().ProbHi() - + # Calculate center coordinates center_coords = [(prob_lo[i] + prob_hi[i]) / 2.0 for i in range(3)] - + # Find the cell index closest to center center_indices = [] for i in range(3): idx = int((center_coords[i] - prob_lo[i]) / dx[i]) center_indices.append(idx) - + # Get value at center (default to 0 if can't access) try: for mfi in multifab: @@ -227,11 +228,11 @@ def postprocess(self, sim_state) -> np.ndarray: if (center_indices[0] >= bx.small_end[0] and center_indices[0] <= bx.big_end[0] and center_indices[1] >= bx.small_end[1] and center_indices[1] <= bx.big_end[1] and center_indices[2] >= bx.small_end[2] and center_indices[2] <= bx.big_end[2]): - + state_arr = xp.array(multifab.array(mfi), copy=False) # Convert global to local indices local_i = center_indices[0] - bx.small_end[0] - local_j = center_indices[1] - bx.small_end[1] + local_j = center_indices[1] - bx.small_end[1] local_k = center_indices[2] - bx.small_end[2] center_val = float(state_arr[0, local_k, local_j, local_i]) break @@ -250,8 +251,14 @@ def postprocess(self, sim_state) -> np.ndarray: break except (IndexError, AttributeError): amr.Print("Warning: Could not access cell (0,0,0,0)") - - return np.array([max_val, mean_val, std_val, sum_val, center_val]) + + return np.array([ + float(max_val), + float(mean_val), + float(std_val), + float(sum_val), + float(center_val) + ]) if __name__ == "__main__": From 8472393ca7dcb3b15a2bbbd28b8e3734c2e8dd07 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 16:27:51 -0700 Subject: [PATCH 063/142] Another update to shape --- .../HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index 909628a9..d146230c 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -143,9 +143,12 @@ def forward(self, x): return outputs def __call__(self, x): + + if x.ndim == 1: + x = x.reshape(1, -1) self.checkDim(x) if hasattr(self, 'domain') and self.domain is not None: - self.checkDomain(x) # ← Added domain checking + self.checkDomain(x) if self.modelpar is None: outputs = self.model(x) From 1676b48aa91c054951e432b5daec97ba50a92b9a Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 16:31:15 -0700 Subject: [PATCH 064/142] Keep simple main.py with inputs --- .../HeatEquation_PythonDriver/Case-0/main.py | 253 ++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100755 GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.py new file mode 100755 index 00000000..03533917 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright 2023 The AMReX Community +# +# This file is part of AMReX. +# +# License: BSD-3-Clause-LBNL +# Authors: Revathi Jambunathan, Edoardo Zoni, Olga Shapoval, David Grote, Axel Huebl + +import amrex.space3d as amr +import numpy as np +from typing import Tuple, Dict, Union + + +def load_cupy(): + """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" + if amr.Config.have_gpu: + try: + import cupy as cp + amr.Print("Note: found and will use cupy") + return cp + except ImportError: + amr.Print("Warning: GPU found but cupy not available! Trying managed memory in numpy...") + import numpy as np + return np + if amr.Config.gpu_backend == "SYCL": + amr.Print("Warning: SYCL GPU backend not yet implemented for Python") + import numpy as np + return np + else: + import numpy as np + amr.Print("Note: found and will use numpy") + return np + + +def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, + plot_int: int = 100, dt: float = 1e-5, plot_files_output: bool = False, + verbose: int = 1, diffusion_coeff: float = 1.0, init_amplitude: float = 1.0, + init_width: float = 0.01) -> Tuple[amr.MultiFab, amr.Geometry]: + """ + Run the heat equation simulation. + The main function, automatically called below if called as a script. + + Returns: + -------- + tuple : (phi_new, geom) + Final state and geometry + """ + # CPU/GPU logic + xp = load_cupy() + + # AMREX_D_DECL means "do the first X of these, where X is the dimensionality of the simulation" + dom_lo = amr.IntVect(*amr.d_decl( 0, 0, 0)) + dom_hi = amr.IntVect(*amr.d_decl(n_cell-1, n_cell-1, n_cell-1)) + + # Make a single box that is the entire domain + domain = amr.Box(dom_lo, dom_hi) + + # Make BoxArray and Geometry: + # ba contains a list of boxes that cover the domain, + # geom contains information such as the physical domain size, + # number of points in the domain, and periodicity + + # Initialize the boxarray "ba" from the single box "domain" + ba = amr.BoxArray(domain) + # Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction + ba.max_size(max_grid_size) + + # This defines the physical box, [0,1] in each direction. + real_box = amr.RealBox([*amr.d_decl( 0., 0., 0.)], [*amr.d_decl( 1., 1., 1.)]) + + # This defines a Geometry object + # periodic in all direction + coord = 0 # Cartesian + is_per = [*amr.d_decl(1,1,1)] # periodicity + geom = amr.Geometry(domain, real_box, coord, is_per); + + # Extract dx from the geometry object + dx = geom.data().CellSize() + + # Nghost = number of ghost cells for each array + Nghost = 1 + + # Ncomp = number of components for each array + Ncomp = 1 + + # How Boxes are distrubuted among MPI processes + dm = amr.DistributionMapping(ba) + + # Allocate two phi multifabs: one will store the old state, the other the new. + phi_old = amr.MultiFab(ba, dm, Ncomp, Nghost) + phi_new = amr.MultiFab(ba, dm, Ncomp, Nghost) + phi_old.set_val(0.) + phi_new.set_val(0.) + + # time = starting time in the simulation + time = 0. + + # Ghost cells + ng = phi_old.n_grow_vect + ngx = ng[0] + ngy = ng[1] + ngz = ng[2] + + # Loop over boxes + for mfi in phi_old: + bx = mfi.validbox() + # phiOld is indexed in reversed order (z,y,x) and indices are local + phiOld = xp.array(phi_old.array(mfi), copy=False) + # set phi = 1 + amplitude * e^(-(r-0.5)^2 / width) + x = (xp.arange(bx.small_end[0],bx.big_end[0]+1,1) + 0.5) * dx[0] + y = (xp.arange(bx.small_end[1],bx.big_end[1]+1,1) + 0.5) * dx[1] + z = (xp.arange(bx.small_end[2],bx.big_end[2]+1,1) + 0.5) * dx[2] + rsquared = ((z[: , xp.newaxis, xp.newaxis] - 0.5)**2 + + (y[xp.newaxis, : , xp.newaxis] - 0.5)**2 + + (x[xp.newaxis, xp.newaxis, : ] - 0.5)**2) / init_width + phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + init_amplitude * xp.exp(-rsquared) + + # Write a plotfile of the initial data if plot_int > 0 and plot_files_output + if plot_int > 0 and plot_files_output: + step = 0 + pltfile = amr.concatenate("plt", step, 5) + varnames = amr.Vector_string(['phi']) + amr.write_single_level_plotfile(pltfile, phi_old, varnames, geom, time, 0) + + for step in range(1, nsteps+1): + # Fill periodic ghost cells + phi_old.fill_boundary(geom.periodicity()) + + # new_phi = old_phi + dt * Laplacian(old_phi) + # Loop over boxes + for mfi in phi_old: + phiOld = xp.array(phi_old.array(mfi), copy=False) + phiNew = xp.array(phi_new.array(mfi), copy=False) + hix = phiOld.shape[3] + hiy = phiOld.shape[2] + hiz = phiOld.shape[1] + + # Heat equation with parameterized diffusion + # Advance the data by dt + phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( + phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + dt * diffusion_coeff * + (( phiOld[:, ngz :-ngz , ngy :-ngy , ngx+1:hix-ngx+1] + -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] + +phiOld[:, ngz :-ngz , ngy :-ngy , ngx-1:hix-ngx-1]) / dx[0]**2 + +( phiOld[:, ngz :-ngz , ngy+1:hiy-ngy+1, ngx :-ngx ] + -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] + +phiOld[:, ngz :-ngz , ngy-1:hiy-ngy-1, ngx :-ngx ]) / dx[1]**2 + +( phiOld[:, ngz+1:hiz-ngz+1, ngy :-ngy , ngx :-ngx ] + -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] + +phiOld[:, ngz-1:hiz-ngz-1, ngy :-ngy , ngx :-ngx ]) / dx[2]**2)) + + # Update time + time = time + dt + + # Copy new solution into old solution + amr.copy_mfab(dst=phi_old, src=phi_new, srccomp=0, dstcomp=0, numcomp=1, nghost=0) + + # Tell the I/O Processor to write out which step we're doing + if(verbose > 0): + amr.Print(f'Advanced step {step}\n') + + # Write a plotfile of the current data (plot_int was defined in the inputs file) + if plot_int > 0 and step%plot_int == 0 and plot_files_output: + pltfile = amr.concatenate("plt", step, 5) + varnames = amr.Vector_string(['phi']) + amr.write_single_level_plotfile(pltfile, phi_new, varnames, geom, time, step) + + return phi_new, geom + + +def parse_inputs() -> Dict[str, Union[int, float, bool]]: + """Parse inputs using AMReX ParmParse to_dict method.""" + pp = amr.ParmParse("") + + # Add inputs file if it exists + import os + inputs_file = "inputs" + if os.path.exists(inputs_file): + pp.addfile(inputs_file) + + # Default values with their types + defaults = { + 'n_cell': 32, + 'max_grid_size': 16, + 'nsteps': 1000, + 'plot_int': 100, + 'dt': 1.0e-5, + 'plot_files_output': False, + 'diffusion_coeff': 1.0, + 'init_amplitude': 1.0, + 'init_width': 0.01 + } + + try: + # Convert entire ParmParse table to Python dictionary + all_params = pp.to_dict() + + # Extract our specific parameters with proper type conversion + params = {} + for key, default_value in defaults.items(): + if key in all_params: + try: + # Convert string to appropriate type based on default + if isinstance(default_value, int): + params[key] = int(all_params[key]) + elif isinstance(default_value, float): + params[key] = float(all_params[key]) + elif isinstance(default_value, bool): + # Handle boolean conversion from string + val_str = str(all_params[key]).lower() + if val_str in ('true', '1', 'yes', 'on'): + params[key] = True + elif val_str in ('false', '0', 'no', 'off'): + params[key] = False + else: + # If unrecognized, use default + params[key] = default_value + else: + params[key] = all_params[key] + except (ValueError, TypeError) as e: + amr.Print(f"Warning: Could not convert parameter {key}='{all_params[key]}' to {type(default_value).__name__}, using default: {default_value}") + params[key] = default_value + else: + params[key] = default_value + + # Optional: print the parameters we're actually using + amr.Print("Using parameters:") + for key, value in params.items(): + amr.Print(f" {key}: {value}") + + return params + + except Exception as e: + amr.Print(f"Warning: Could not parse parameters with to_dict(): {e}") + amr.Print("Using default values") + return defaults + +if __name__ == '__main__': + # Initialize AMReX + amr.initialize([]) + + try: + # Parse inputs + params = parse_inputs() + + # Run simulation + main(**params) + + finally: + # Finalize AMReX + amr.finalize() From ef7320eec7bc5e4aa6aabc2b95081f59b4c9d513 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 16:33:18 -0700 Subject: [PATCH 065/142] Copy from other case --- .../Case-0/GNUmakefile | 16 + .../HeatEquation_PythonDriver/Case-0/inputs | 16 + .../HeatEquation_PythonDriver/Case-0/main.cpp | 320 ++++++++++++++++++ 3 files changed, 352 insertions(+) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-0/GNUmakefile create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-0/inputs create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/GNUmakefile b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/GNUmakefile new file mode 100644 index 00000000..f918d4f2 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/GNUmakefile @@ -0,0 +1,16 @@ +# AMREX_HOME defines the directory in which we will find all the AMReX code. +AMREX_HOME ?= ../../../../amrex + +DEBUG = FALSE +USE_MPI = FALSE +USE_OMP = FALSE +COMP = gnu +DIM = 3 + +include $(AMREX_HOME)/Tools/GNUMake/Make.defs + +include ./Make.package + +include $(AMREX_HOME)/Src/Base/Make.package + +include $(AMREX_HOME)/Tools/GNUMake/Make.rules diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/inputs b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/inputs new file mode 100644 index 00000000..56ec738e --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/inputs @@ -0,0 +1,16 @@ +# Grid/Domain parameters +n_cell = 32 # number of cells on each side of domain +max_grid_size = 16 # size of each box (or grid) + +# Time stepping parameters +nsteps = 100 # total steps in simulation +dt = 1.e-5 # time step + +# Output control +plot_int = 100 # how often to write plotfile (-1 = no plots) +datalog_int = -1 # how often to write datalog (-1 = no regular output) + +# Physics parameters (these are what we vary for UQ) +diffusion_coeff = 1.0 # diffusion coefficient for heat equation +init_amplitude = 1.0 # amplitude of initial temperature profile +init_width = 0.01 # width parameter (variance, not std dev) \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp new file mode 100644 index 00000000..d4e69e3e --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp @@ -0,0 +1,320 @@ +/* + * A simplified single file version of the HeatEquation_EX0_C exmaple. + * This code is designed to be used with Demo_Tutorial.rst. + * + */ + + +#include +#include +#include + +int main (int argc, char* argv[]) +{ + amrex::Initialize(argc,argv); + { + + // ********************************** + // DECLARE SIMULATION PARAMETERS + // ********************************** + + // number of cells on each side of the domain + int n_cell; + + // size of each box (or grid) + int max_grid_size; + + // total steps in simulation + int nsteps; + + // how often to write a plotfile + int plot_int; + + // time step + amrex::Real dt; + + // ********************************** + // DECLARE PHYSICS PARAMETERS + // ********************************** + + // diffusion coefficient for heat equation + amrex::Real diffusion_coeff; + + // amplitude of initial temperature profile + amrex::Real init_amplitude; + + // width parameter controlling spread of initial profile (variance, not std dev) + amrex::Real init_width; + + // ********************************** + // DECLARE DATALOG PARAMETERS + // ********************************** + const int datwidth = 14; + const int datprecision = 6; + const int timeprecision = 13; + int datalog_int = -1; // Interval for regular output (<=0 means no regular output) + bool datalog_final = true; // Write datalog at final step + + // ********************************** + // READ PARAMETER VALUES FROM INPUT DATA + // ********************************** + // inputs parameters + { + // ParmParse is way of reading inputs from the inputs file + // pp.get means we require the inputs file to have it + // pp.query means we optionally need the inputs file to have it - but we must supply a default here + amrex::ParmParse pp; + + // We need to get n_cell from the inputs file - this is the number of cells on each side of + // a square (or cubic) domain. + pp.get("n_cell",n_cell); + + // The domain is broken into boxes of size max_grid_size + pp.get("max_grid_size",max_grid_size); + + // Default nsteps to 10, allow us to set it to something else in the inputs file + nsteps = 10; + pp.query("nsteps",nsteps); + + // Default plot_int to -1, allow us to set it to something else in the inputs file + // If plot_int < 0 then no plot files will be written + plot_int = -1; + pp.query("plot_int",plot_int); + + // Default datalog_int to -1, allow us to set it to something else in the inputs file + // If datalog_int < 0 then no plot files will be written + datalog_int = -1; + pp.query("datalog_int",datalog_int); + + // time step + pp.get("dt",dt); + + // ********************************** + // READ PHYSICS PARAMETERS + // ********************************** + + // Diffusion coefficient - controls how fast heat spreads + diffusion_coeff = 1.0; + pp.query("diffusion_coeff", diffusion_coeff); // Note: input name is "diffusion" but variable is "diffusion_coeff" + + // Initial temperature amplitude + init_amplitude = 1.0; + pp.query("init_amplitude", init_amplitude); + + // Width parameter - this is the variance (width²), not standard deviation + // Smaller values = more concentrated, larger values = more spread out + init_width = 0.01; // Note: 0.01 to match your original rsquared/0.01 + pp.query("init_width", init_width); + } + + // ********************************** + // DEFINE SIMULATION SETUP AND GEOMETRY + // ********************************** + + // make BoxArray and Geometry + // ba will contain a list of boxes that cover the domain + // geom contains information such as the physical domain size, + // number of points in the domain, and periodicity + amrex::BoxArray ba; + amrex::Geometry geom; + + // define lower and upper indices + amrex::IntVect dom_lo(0,0,0); + amrex::IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); + + // Make a single box that is the entire domain + amrex::Box domain(dom_lo, dom_hi); + + // Initialize the boxarray "ba" from the single box "domain" + ba.define(domain); + + // Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction + ba.maxSize(max_grid_size); + + // This defines the physical box, [0,1] in each direction. + amrex::RealBox real_box({ 0., 0., 0.}, + { 1., 1., 1.}); + + // periodic in all direction + amrex::Array is_periodic{1,1,1}; + + // This defines a Geometry object + geom.define(domain, real_box, amrex::CoordSys::cartesian, is_periodic); + + // extract dx from the geometry object + amrex::GpuArray dx = geom.CellSizeArray(); + + // Nghost = number of ghost cells for each array + int Nghost = 1; + + // Ncomp = number of components for each array + int Ncomp = 1; + + // How Boxes are distrubuted among MPI processes + amrex::DistributionMapping dm(ba); + + // we allocate two phi multifabs; one will store the old state, the other the new. + amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); + amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); + + // time = starting time in the simulation + amrex::Real time = 0.0; + + // ********************************** + // INITIALIZE DATA LOOP + // ********************************** + + // loop over boxes + for (amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi) + { + const amrex::Box& bx = mfi.validbox(); + + const amrex::Array4& phiOld = phi_old.array(mfi); + + // ********************************** + // SET INITIAL TEMPERATURE PROFILE + // ********************************** + // Formula: T = 1 + amplitude * exp(-r^2 / width^2) + // where r is distance from center (0.5, 0.5, 0.5) + // + // Parameters: + // - amplitude: controls peak temperature above baseline (1.0) + // - width: controls spread of initial hot spot + // - smaller width = more concentrated + // - larger width = more spread out + +amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) + { + // Calculate physical coordinates of cell center + amrex::Real x = (i+0.5) * dx[0]; + amrex::Real y = (j+0.5) * dx[1]; + amrex::Real z = (k+0.5) * dx[2]; + + // Calculate squared distance from domain center (0.5, 0.5, 0.5) + // Divide by init_width (which is the variance, not standard deviation) + amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; + + // Set initial temperature profile + phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); + }); + } + + // ********************************** + // WRITE DATALOG FILE + // ********************************** + if (amrex::ParallelDescriptor::IOProcessor() && (datalog_int>0 || datalog_final)) { + std::ofstream datalog("datalog.txt"); // truncate mode to start fresh + datalog << "#" << std::setw(datwidth-1) << " time"; + datalog << std::setw(datwidth) << " max_temp"; + datalog << std::setw(datwidth) << " std_temp"; + datalog << std::setw(datwidth) << " final_step"; + datalog << std::endl; + datalog.close(); + } + + // ********************************** + // WRITE INITIAL PLOT FILE + // ********************************** + + // Write a plotfile of the initial data if plot_int > 0 + if (plot_int > 0) + { + int step = 0; + const std::string& pltfile = amrex::Concatenate("plt",step,5); + WriteSingleLevelPlotfile(pltfile, phi_old, {"phi"}, geom, time, 0); + } + + + // ********************************** + // MAIN TIME EVOLUTION LOOP + // ********************************** + + for (int step = 1; step <= nsteps; ++step) + { + // fill periodic ghost cells + phi_old.FillBoundary(geom.periodicity()); + + // new_phi = old_phi + dt * Laplacian(old_phi) + // loop over boxes + for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ) + { + const amrex::Box& bx = mfi.validbox(); + + const amrex::Array4& phiOld = phi_old.array(mfi); + const amrex::Array4& phiNew = phi_new.array(mfi); + + // advance the data by dt using heat equation with diffusion coefficient + amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) + { + // Calculate the discrete Laplacian using finite differences + amrex::Real laplacian = + (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) + +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) + +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]); + + // Apply heat equation using diffusion_coeff - matches Python version + phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; + }); + } + + // ********************************** + // INCREMENT + // ********************************** + + // update time + time = time + dt; + + // copy new solution into old solution + amrex::MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); + + // Tell the I/O Processor to write out which step we're doing + amrex::Print() << "Advanced step " << step << "\n"; + + // ********************************** + // WRITE DATALOG AT GIVEN INTERVAL + // ********************************** + + // Check if we should write datalog + bool write_datalog = false; + if (datalog_final && step == nsteps) { + write_datalog = true; // Write final step + } else if (datalog_int > 0 && step % datalog_int == 0) { + write_datalog = true; // Write every datalog_int steps + } + + if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { + std::ofstream datalog("datalog.txt", std::ios::app); + + // Calculate temperature statistics + amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); + amrex::Real max_temperature = phi_new.max(0); + amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; + amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; + + datalog << std::setw(datwidth) << std::setprecision(timeprecision) << time; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << max_temperature; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << std_temperature; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << step; + datalog << std::endl; + + datalog.close(); + } + + // ********************************** + // WRITE PLOTFILE AT GIVEN INTERVAL + // ********************************** + + // Write a plotfile of the current data (plot_int was defined in the inputs file) + if (plot_int > 0 && step%plot_int == 0) + { + const std::string& pltfile = amrex::Concatenate("plt",step,5); + WriteSingleLevelPlotfile(pltfile, phi_new, {"phi"}, geom, time, step); + } + } + + } + amrex::Finalize(); + return 0; +} + + From 94310c54ae8cf007e76adbc5b787b5a8f1834b27 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 16:44:28 -0700 Subject: [PATCH 066/142] Make outputs the same --- .../Case-0/HeatEquationModel.py | 57 +------------------ .../HeatEquation_PythonDriver/Case-0/main.cpp | 18 +++--- 2 files changed, 12 insertions(+), 63 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py index 620902d3..dbd210d6 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py @@ -24,7 +24,6 @@ class HeatEquationModel(AMReXBaseModel): ('output', 'mean_temperature'), ('output', 'std_temperature'), ('output', 'total_energy'), - ('output', 'center_temperature'), ] # Spatial domain bounds (3D heat equation domain) @@ -129,10 +128,6 @@ def _get_field_info(self, field_tuple): 'units': 'J', 'display_name': 'Total Energy', }, - ('output', 'center_temperature'): { - 'units': 'K', - 'display_name': 'Center Temperature', - }, } return field_info_dict.get(field_tuple, {}) @@ -188,7 +183,7 @@ def postprocess(self, sim_state) -> np.ndarray: Returns: -------- np.ndarray - Processed outputs [max, mean, std, integral/sum, center_value] + Processed outputs [max, mean, std, integral/sum] """ xp = load_cupy() [multifab, varnames, geom] = sim_state @@ -205,59 +200,11 @@ def postprocess(self, sim_state) -> np.ndarray: variance = (sum_sq / total_cells) - mean_val**2 std_val = np.sqrt(max(0, variance)) - # Get value at center (if geometry available) - center_val = 0.0 - if geom is not None: - dx = geom.data().CellSize() - prob_lo = geom.data().ProbLo() - prob_hi = geom.data().ProbHi() - - # Calculate center coordinates - center_coords = [(prob_lo[i] + prob_hi[i]) / 2.0 for i in range(3)] - - # Find the cell index closest to center - center_indices = [] - for i in range(3): - idx = int((center_coords[i] - prob_lo[i]) / dx[i]) - center_indices.append(idx) - - # Get value at center (default to 0 if can't access) - try: - for mfi in multifab: - bx = mfi.validbox() - if (center_indices[0] >= bx.small_end[0] and center_indices[0] <= bx.big_end[0] and - center_indices[1] >= bx.small_end[1] and center_indices[1] <= bx.big_end[1] and - center_indices[2] >= bx.small_end[2] and center_indices[2] <= bx.big_end[2]): - - state_arr = xp.array(multifab.array(mfi), copy=False) - # Convert global to local indices - local_i = center_indices[0] - bx.small_end[0] - local_j = center_indices[1] - bx.small_end[1] - local_k = center_indices[2] - bx.small_end[2] - center_val = float(state_arr[0, local_k, local_j, local_i]) - break - except (IndexError, AttributeError): - # Fall back to (0,0,0) if center calculation fails - for mfi in multifab: - state_arr = xp.array(multifab.array(mfi), copy=False) - center_val = float(state_arr[0, 0, 0, 0]) - break - else: - # No geometry available, use (0,0,0,0) cell - try: - for mfi in multifab: - state_arr = xp.array(multifab.array(mfi), copy=False) - center_val = float(state_arr[0, 0, 0, 0]) - break - except (IndexError, AttributeError): - amr.Print("Warning: Could not access cell (0,0,0,0)") - return np.array([ float(max_val), float(mean_val), float(std_val), - float(sum_val), - float(center_val) + float(sum_val) ]) if __name__ == "__main__": diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp index d4e69e3e..b8392b65 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp @@ -204,10 +204,10 @@ amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) // ********************************** if (amrex::ParallelDescriptor::IOProcessor() && (datalog_int>0 || datalog_final)) { std::ofstream datalog("datalog.txt"); // truncate mode to start fresh - datalog << "#" << std::setw(datwidth-1) << " time"; - datalog << std::setw(datwidth) << " max_temp"; - datalog << std::setw(datwidth) << " std_temp"; - datalog << std::setw(datwidth) << " final_step"; + datalog << "#" << std::setw(datwidth-1) << " max_temp"; + datalog << std::setw(datwidth) << " mean_temp"; + datalog << std::setw(datwidth) << " std_temp"; + datalog << std::setw(datwidth) << " total_energy"; datalog << std::endl; datalog.close(); } @@ -290,11 +290,13 @@ amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) amrex::Real max_temperature = phi_new.max(0); amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; + amrex::Real total_energy = phi_new.sum(0); - datalog << std::setw(datwidth) << std::setprecision(timeprecision) << time; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << max_temperature; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << std_temperature; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << step; + // Write 4 statistics + datalog << std::setw(datwidth) << std::setprecision(datprecision) << max_temperature; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << mean_temp; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << std_temperature; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << total_energy; datalog << std::endl; datalog.close(); From e892722b33082a7d62d6aced6aa3d2f99083b865 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Thu, 2 Oct 2025 16:54:29 -0700 Subject: [PATCH 067/142] Try testing again, needed make --- .../Case-0/AMReXBaseModel.py | 10 +++++----- .../Case-0/HeatEquationModel.py | 11 ++++++++++- .../HeatEquation_PythonDriver/Case-0/Make.package | 2 ++ 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-0/Make.package diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py index d146230c..d060ed33 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py @@ -36,6 +36,11 @@ class AMReXBaseModel(ModelWrapperFcn): _spatial_domain_bounds = None def __init__(self, model=None, **kwargs): + # Initialize AMReX if needed + self.xp = load_cupy() + if not amr.initialized(): + amr.initialize([]) + # Create modelpar from existing parameter information modelpar = self._create_modelpar() @@ -82,11 +87,6 @@ def __init__(self, model=None, **kwargs): if len(self._spatial_domain_bounds) > 2 else None) - # Initialize AMReX if needed - self.xp = load_cupy() - if not amr.initialized(): - amr.initialize([]) - def _create_field_info(self): """Create yt-style field info container""" field_info = {} diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py index dbd210d6..9067e768 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py @@ -35,7 +35,7 @@ class HeatEquationModel(AMReXBaseModel): def __init__(self, n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 1000, plot_int: int = 100, - dt: float = 1e-5, use_parmparse: bool = False, **kwargs): + dt: float = 1e-5, use_parmparse: bool = True, **kwargs): """ Initialize heat equation model. @@ -56,6 +56,11 @@ def __init__(self, n_cell: int = 32, max_grid_size: int = 16, """ # Store simulation parameters if use_parmparse: + # Conditionally initialize AMReX first if using ParmParse + if not amr.initialized(): + amr.initialize([]) + + # Parse inputs from file from main import parse_inputs params = parse_inputs() self.n_cell = params['n_cell'] @@ -70,6 +75,10 @@ def __init__(self, n_cell: int = 32, max_grid_size: int = 16, self.plot_int = plot_int self.dt = dt + # Conditionally initialize AMReX + if not amr.initialized(): + amr.initialize([]) + # Update spatial domain dimensions based on n_cell if self.n_cell != 32: self._spatial_domain_bounds[2] = np.array([self.n_cell, self.n_cell, self.n_cell]) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/Make.package b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/Make.package new file mode 100644 index 00000000..7f43e5e8 --- /dev/null +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/Make.package @@ -0,0 +1,2 @@ +CEXE_sources += main.cpp + From 9b2ff7ccde528f087cf677c31be1f53adc1fd371 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 3 Oct 2025 07:48:12 -0700 Subject: [PATCH 068/142] Move to Case-0 --- .../HeatEquation_PythonDriver/{Case-1 => Case-0}/model_wrapper.sh | 0 .../{Case-1 => Case-0}/postprocess_datalog.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-1 => Case-0}/model_wrapper.sh (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-1 => Case-0}/postprocess_datalog.sh (100%) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model_wrapper.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/model_wrapper.sh similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/model_wrapper.sh rename to GuidedTutorials/HeatEquation_PythonDriver/Case-0/model_wrapper.sh diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/postprocess_datalog.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/postprocess_datalog.sh similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/postprocess_datalog.sh rename to GuidedTutorials/HeatEquation_PythonDriver/Case-0/postprocess_datalog.sh From b2dd652cce791b7d3d3a6258a91ce69d93a9dfb5 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 3 Oct 2025 09:13:28 -0700 Subject: [PATCH 069/142] datalog output and parameter inputs match --- .../Case-0/HeatEquationModel.py | 12 ++++++------ .../HeatEquation_PythonDriver/Case-0/main.cpp | 8 +++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py index 9067e768..ec9acd75 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py @@ -13,16 +13,16 @@ class HeatEquationModel(AMReXBaseModel): # Define parameter fields with proper naming convention _param_fields = [ - ('param', 'diffusion_coefficient'), - ('param', 'initial_amplitude'), - ('param', 'initial_width'), + ('param', 'diffusion_coeff'), + ('param', 'init_amplitude'), + ('param', 'init_width'), ] # Define output fields _output_fields = [ - ('output', 'max_temperature'), - ('output', 'mean_temperature'), - ('output', 'std_temperature'), + ('output', 'max_temp'), + ('output', 'mean_temp'), + ('output', 'std_temp'), ('output', 'total_energy'), ] diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp index b8392b65..cf0ece87 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp @@ -49,8 +49,8 @@ int main (int argc, char* argv[]) // ********************************** // DECLARE DATALOG PARAMETERS // ********************************** - const int datwidth = 14; - const int datprecision = 6; + const int datwidth = 24; + const int datprecision = 16; const int timeprecision = 13; int datalog_int = -1; // Interval for regular output (<=0 means no regular output) bool datalog_final = true; // Write datalog at final step @@ -288,7 +288,9 @@ amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) // Calculate temperature statistics amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); amrex::Real max_temperature = phi_new.max(0); - amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; + amrex::Real l2_norm = phi_new.norm2(0); + amrex::Real sum_sq = l2_norm * l2_norm; + amrex::Real variance = sum_sq / phi_new.boxArray().numPts() - mean_temp * mean_temp; amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; amrex::Real total_energy = phi_new.sum(0); From 539adeb612be1ba030650b341a23f09c9dd791b8 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 3 Oct 2025 10:21:00 -0700 Subject: [PATCH 070/142] Fix names again --- .../Case-0/HeatEquationModel.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py index ec9acd75..1ada03f5 100644 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py +++ b/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py @@ -95,7 +95,7 @@ def _get_field_info(self, field_tuple): field_info_dict = { # Parameters - match param_margpc.txt values - ('param', 'diffusion_coefficient'): { + ('param', 'diffusion_coeff'): { 'mean': 1.0, 'std': 0.25, 'bounds': [0.1, 5.0], @@ -103,7 +103,7 @@ def _get_field_info(self, field_tuple): 'units': 'm**2/s', 'display_name': 'Thermal Diffusivity', }, - ('param', 'initial_amplitude'): { + ('param', 'init_amplitude'): { 'mean': 1.0, 'std': 0.25, 'bounds': [0.1, 3.0], @@ -111,7 +111,7 @@ def _get_field_info(self, field_tuple): 'units': 'K', 'display_name': 'Initial Temperature', }, - ('param', 'initial_width'): { + ('param', 'init_width'): { 'mean': 0.01, 'std': 0.0025, 'bounds': [0.001, 0.1], @@ -121,15 +121,15 @@ def _get_field_info(self, field_tuple): }, # Outputs - just units and display names - ('output', 'max_temperature'): { + ('output', 'max_temp'): { 'units': 'K', 'display_name': 'Maximum Temperature', }, - ('output', 'mean_temperature'): { + ('output', 'mean_temp'): { 'units': 'K', 'display_name': 'Mean Temperature', }, - ('output', 'std_temperature'): { + ('output', 'std_temp'): { 'units': 'K', 'display_name': 'Temperature Std Dev', }, From 764ac674a9e2ea87968f6f906ceadf3cb37fb3a6 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 6 Oct 2025 07:10:54 -0700 Subject: [PATCH 071/142] Remove duplicates and examples that are not fully tested --- .../Case-1/Visualization.ipynb | 200 ------- .../Case-1/functions.sh | 561 ------------------ .../HeatEquation_PythonDriver/Case-1/main.cpp | 320 ---------- .../HeatEquation_PythonDriver/Case-1/model.x | 163 ----- .../Case-2/AMReXBaseModel.py | 239 -------- .../Case-2/AMReXTorchModel.py | 32 - .../Case-2/HeatEquationModel.py | 396 ------------- .../Case-2/README.md | 1 - .../HeatEquation_PythonDriver/Case-2/inputs | 11 - .../HeatEquation_PythonDriver/Case-2/main.py | 253 -------- .../Case-3/CMakeLists.txt | 77 --- .../Case-3/GNUmakefile | 126 ---- .../Case-3/Make.package | 6 - .../Case-3/README.md | 89 --- .../Case-3/bindings.H | 15 - .../Case-3/bindings.cpp | 177 ------ .../HeatEquation_PythonDriver/Case-3/inputs | 7 - .../HeatEquation_PythonDriver/Case-3/main.cpp | 288 --------- .../Case-3/pybind11.cmake | 63 -- .../HeatEquation_PythonDriver/Case-3/test.py | 37 -- 20 files changed, 3061 deletions(-) delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/Visualization.ipynb delete mode 100755 GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp delete mode 100755 GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXBaseModel.py delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXTorchModel.py delete mode 100755 GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-2/README.md delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs delete mode 100755 GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-3/CMakeLists.txt delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-3/GNUmakefile delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-3/Make.package delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-3/README.md delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-3/bindings.H delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-3/bindings.cpp delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-3/inputs delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-3/main.cpp delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-3/pybind11.cmake delete mode 100644 GuidedTutorials/HeatEquation_PythonDriver/Case-3/test.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Visualization.ipynb b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Visualization.ipynb deleted file mode 100644 index e9dddb47..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Visualization.ipynb +++ /dev/null @@ -1,200 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "839a3961", - "metadata": {}, - "source": [ - "## Example: 3D Heat Equation\n", - "\n", - "### What Features Are We Using\n", - "\n", - "* Mesh data \n", - "* Domain Decomposition\n", - "\n", - "We now look at a more complicated example at and show how simulation results\n", - "can be visualized. This example solves the heat equation,\n", - "\n", - "$$\\frac{\\partial\\phi}{\\partial t} = \\nabla^2\\phi$$\n", - "\n", - "using forward Euler temporal integration on a periodic domain. We could use a\n", - "5-point (in 2D) or 7-point (in 3D) stencil, but for demonstration purposes we\n", - "spatially discretize the PDE by first constructing (negative) fluxes on cell faces, e.g.,\n", - "\n", - "$$F_{i+^1\\!/_2,\\,j} = \\frac{\\phi_{i+1,j}-\\phi_{i,j}}{\\Delta x}$$\n", - "\n", - "and then taking the divergence to update the cells:\n", - "\n", - " $$\\phi_{i,\\,j}^{n+1} = \\phi_{i,\\,j}^n\n", - " + \\frac{\\Delta t}{\\Delta x}\\left(F_{i+^1\\!/_2,\\,j}-F_{i-^1\\!/_2,\\,j}\\right)\n", - " + \\frac{\\Delta t}{\\Delta y}\\left(F_{i,\\,j+^1\\!/_2}-F_{i,\\,j-^1\\!/_2}\\right)$$" - ] - }, - { - "cell_type": "markdown", - "id": "08b4ec5f", - "metadata": {}, - "source": [ - "The code to generate the initial condition is in `mykernel.H` and looks like: \n", - "```C++\n", - "{\n", - " Real x = prob_lo[0] + (i+Real(0.5)) * dx[0];\n", - " Real y = prob_lo[1] + (j+Real(0.5)) * dx[1];\n", - " Real z = prob_lo[2] + (k+Real(0.5)) * dx[2];\n", - " Real r2 = ((x-Real(0.25))*(x-Real(0.25))+(y-Real(0.25))*(y-Real(0.25))+(z-Real(0.25))*(z-Real(0.25)))/Real(0.01);\n", - " phi(i,j,k) = Real(1.) + std::exp(-r2);\n", - "}\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "33bd71f2", - "metadata": {}, - "source": [ - "## Running the code\n", - "\n", - "The simulation can be ran as `./03_HeatEquation inputs`. \n", - "\n", - "The following inputs parameters could be tweaked:\n", - "\n", - "```\n", - "nsteps = 1000 # number of time steps to take\n", - "plot_int = 100 # write plots every n steps\n", - "n_cell = 128 # number of cells in the domain\n", - "max_grid_size = 64 # max grid size used for domain decomposition\n", - "\n", - "```\n", - "\n", - "Although we are running this example in serial, we decompose the domain into multiple boxes, anticipating more complicated problems where we have mesh refinement:\n", - "\n", - "```C++\n", - " IntVect dom_lo(AMREX_D_DECL( 0, 0, 0));\n", - " IntVect dom_hi(AMREX_D_DECL(n_cell-1, n_cell-1, n_cell-1));\n", - " Box domain(dom_lo, dom_hi);\n", - "\n", - " // Initialize the boxarray \"ba\" from the single box \"bx\"\n", - " ba.define(domain);\n", - " // Break up boxarray \"ba\" into chunks no larger than \"max_grid_size\" along a direction\n", - " ba.maxSize(max_grid_size);\n", - "\n", - "```" - ] - }, - { - "cell_type": "markdown", - "id": "b772b897", - "metadata": {}, - "source": [ - "## Visualizating the results\n", - "\n", - "Below we give some python code to visualizate the solution using yt:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "29609c78", - "metadata": {}, - "outputs": [], - "source": [ - "import yt\n", - "from yt.frontends import boxlib\n", - "from yt.frontends.boxlib.data_structures import AMReXDataset" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "18ec883c", - "metadata": {}, - "outputs": [], - "source": [ - "ls" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "cb533aa1", - "metadata": {}, - "outputs": [], - "source": [ - "ds = AMReXDataset(\"plt00000\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2d834eaf", - "metadata": {}, - "outputs": [], - "source": [ - "ds.field_list" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "899767fc", - "metadata": {}, - "outputs": [], - "source": [ - "sl = yt.SlicePlot(ds, 2, ('boxlib', 'phi'))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "679eaca7", - "metadata": {}, - "outputs": [], - "source": [ - "sl" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "72af42b5", - "metadata": {}, - "outputs": [], - "source": [ - "ds = AMReXDataset(\"plt01000\")\n", - "sl = yt.SlicePlot(ds, 2, ('boxlib', 'phi'))\n", - "sl" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "bd7332dd", - "metadata": {}, - "outputs": [], - "source": [ - "sl.annotate_grids()" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh deleted file mode 100755 index 1a251568..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/functions.sh +++ /dev/null @@ -1,561 +0,0 @@ -#!/bin/bash -# functions.sh - AMReX plotfile processing functions - -# Function to extract data from datalog.txt -function extract_data_from_datalog { - local run_dir=$1 - local run_counter=$2 - - local datalog_file="$run_dir/datalog.txt" - - echo "Processing datalog in $run_dir" >&2 - - if [ ! -f "$datalog_file" ]; then - echo "Error: datalog.txt not found in $run_dir" >&2 - return 1 - fi - - # Generate outnames from header on first run - if [ $run_counter -eq 0 ] && [ ! -f "outnames.txt" ]; then - echo "Generating outnames from datalog header..." >&2 - generate_outnames_from_datalog_header "$datalog_file" - fi - - echo "Datalog file contents:" >&2 - cat "$datalog_file" >&2 - - # Get the last line of data (skip comment lines) - local last_line=$(grep -v '^#' "$datalog_file" | tail -1) - - if [ -z "$last_line" ]; then - echo "Error: No data lines found in datalog.txt" >&2 - return 1 - fi - - echo "Last data line: $last_line" >&2 - - # Return all the values from the last line - echo "$last_line" - return 0 -} - -# Function to generate outnames from datalog header -function generate_outnames_from_datalog_header { - local datalog_file=$1 - - if [ ! -f "$datalog_file" ]; then - echo "Error: datalog file $datalog_file not found" >&2 - return 1 - fi - - echo "Generating outnames from datalog header: $datalog_file" >&2 - - # Get the header line (starts with #) - local header_line=$(grep '^#' "$datalog_file" | head -1) - - if [ -z "$header_line" ]; then - echo "Error: No header line found in datalog" >&2 - return 1 - fi - - echo "Found header: $header_line" >&2 - - # Remove the # and split into column names - local columns=$(echo "$header_line" | sed 's/^#//' | tr -s ' ' | sed 's/^ *//' | sed 's/ *$//') - - echo "Parsed columns: $columns" >&2 - - # Convert to array and write to outnames.txt - local column_array=($columns) - - > outnames.txt - for col in "${column_array[@]}"; do - echo "$col" >> outnames.txt - echo "Added column: $col" >&2 - done - - echo "Generated outnames.txt:" >&2 - cat outnames.txt >&2 - - return 0 -} - -# Function to generate output names using variable names from Header and components from Cell_H -function generate_outnames_from_combined_sources { - local plotfile_path=$1 - local level=$2 - - local level_header="$plotfile_path/Level_${level}/Cell_H" - local main_header="$plotfile_path/Header" - - if [ ! -f "$level_header" ] || [ ! -f "$main_header" ]; then - echo "Error: Required header files not found" >&2 - return 1 - fi - - echo "DEBUG: Reading from Header: $main_header" >&2 - echo "DEBUG: Reading from Cell_H: $level_header" >&2 - - # Get number of variables from Header (line 2) - local num_variables=$(sed -n '2p' "$main_header") - echo "DEBUG: Number of variables from Header: $num_variables" >&2 - - # Get actual number of components from Cell_H data - local dimensions_line=$(grep "^[0-9]*,[0-9]*$" "$level_header" | head -1) - local num_components=$(echo $dimensions_line | cut -d, -f2) - echo "DEBUG: Actual components from Cell_H: $num_components" >&2 - echo "DEBUG: Cell_H dimensions line: $dimensions_line" >&2 - - # Generate output names file - > outnames.txt - - # Read variable names from Header (starting at line 3) - local line_num=3 - for ((var=0; var&2 - - # Skip the next line in Header (which might be dimensions, not components) - ((line_num++)) - local header_value=$(sed -n "${line_num}p" "$main_header") - echo "DEBUG: Skipping Header line $line_num: '$header_value' (probably not components)" >&2 - ((line_num++)) - - # Generate min/max entries using ACTUAL components from Cell_H - for ((comp=0; comp> outnames.txt - echo "${var_name}_${comp}_max" >> outnames.txt - echo "DEBUG: Added ${var_name}_${comp}_min and ${var_name}_${comp}_max" >&2 - done - - # ALL OUTPUT TO STDERR - echo "Added variable '$var_name' with $num_components actual components" >&2 - done - - # ALL OUTPUT TO STDERR - echo "Generated outnames.txt with $num_variables variables and $num_components components each" >&2 - echo "DEBUG: Final outnames.txt contents:" >&2 - cat outnames.txt >&2 -} - -# Function to generate output names from plotfile header -function generate_outnames_from_header { - local header_file=$1 - - if [ ! -f "$header_file" ]; then - echo "Error: Header file $header_file not found!" - return 1 - fi - - # Read number of variables from line 2 - local num_variables=$(sed -n '2p' "$header_file") - - # Generate output names file - > outnames.txt - - local line_num=3 - for ((var=0; var> outnames.txt - echo "${var_name}_${comp}_max" >> outnames.txt - done - - echo "Added variable '$var_name' with $num_components components" - done - - echo "Generated outnames.txt with $num_variables variables" -} - -# Function to find which box contains the target ijk coordinates from Cell_H file -function find_box_index { - local target_i=$1 - local target_j=$2 - local target_k=$3 - local cell_header_file=$4 - - if [ ! -f "$cell_header_file" ]; then - echo "Error: Cell header file $cell_header_file not found!" >&2 - echo -1 - return - fi - - # Find the line with box array definition like "(8 0" - local box_array_line=$(grep -n "^([0-9]* [0-9]*$" "$cell_header_file" | cut -d: -f1) - - if [ -z "$box_array_line" ]; then - echo "Error: Could not find box array definition in $cell_header_file" >&2 - echo -1 - return - fi - - # Extract number of boxes from that line - local num_boxes=$(sed -n "${box_array_line}p" "$cell_header_file" | sed 's/^(\([0-9]*\) [0-9]*$/\1/') - - echo "Found $num_boxes boxes in $cell_header_file" >&2 - - # Read each box definition and check if target coordinates fall within - local box_index=0 - local current_line=$((box_array_line + 1)) - - for ((box=0; box&2 - - # Check if target coordinates fall within this box - if [ $target_i -ge $i_lo ] && [ $target_i -le $i_hi ] && \ - [ $target_j -ge $j_lo ] && [ $target_j -le $j_hi ] && \ - [ $target_k -ge $k_lo ] && [ $target_k -le $k_hi ]; then - echo "Target ($target_i,$target_j,$target_k) found in box $box" >&2 - echo $box - return - fi - else - echo "Error: Could not parse box definition: $box_def" >&2 - fi - - ((current_line++)) - done - - # If not found, return -1 - echo "Target coordinates ($target_i,$target_j,$target_k) not found in any box" >&2 - echo -1 -} - -# Function to extract min/max values from cell header -function extract_minmax_from_cell_header { - local plotfile_path=$1 - local level=$2 - local target_i=$3 - local target_j=$4 - local target_k=$5 - local output_names_file=$6 - - local level_header="$plotfile_path/Level_${level}/Cell_H" - - if [ ! -f "$level_header" ]; then - echo "Error: Level header $level_header not found" - return 1 - fi - - # Find which box contains the target coordinates - local box_index=$(find_box_index $target_i $target_j $target_k "$level_header") - - if [ $box_index -eq -1 ]; then - echo "Error: Target coordinates not found in any box" - return 1 - fi - - # Find the min/max sections in the level header - local min_start_line=$(grep -n "^[0-9]*,[0-9]*$" "$level_header" | head -1 | cut -d: -f1) - local max_start_line=$(grep -n "^[0-9]*,[0-9]*$" "$level_header" | tail -1 | cut -d: -f1) - - if [ -z "$min_start_line" ] || [ -z "$max_start_line" ]; then - echo "Error: Could not find min/max sections in level header" - return 1 - fi - - # Get the dimensions from the first line - local dimensions=$(sed -n "${min_start_line}p" "$level_header") - local num_boxes=$(echo $dimensions | cut -d, -f1) - local num_components=$(echo $dimensions | cut -d, -f2) - - # Extract min values (lines after the first dimension line) - local min_end_line=$((min_start_line + num_boxes)) - sed -n "$((min_start_line + 1)),${min_end_line}p" "$level_header" > temp_min.txt - - # Extract max values (lines after the second dimension line) - local max_end_line=$((max_start_line + num_boxes)) - sed -n "$((max_start_line + 1)),${max_end_line}p" "$level_header" > temp_max.txt - - # Get the value from the specific box (line number = box_index + 1) - local target_line=$((box_index + 1)) - - if [ $target_line -gt $num_boxes ]; then - echo "Error: Box index $box_index exceeds available data lines" - rm -f temp_min.txt temp_max.txt - return 1 - fi - - # Extract all component values for the target box - local min_line=$(sed -n "${target_line}p" temp_min.txt) - local max_line=$(sed -n "${target_line}p" temp_max.txt) - - # Parse components (comma-separated) - IFS=',' read -ra min_components <<< "$min_line" - IFS=',' read -ra max_components <<< "$max_line" - - # Read output names - readarray -t output_names < "$output_names_file" - - # Build output line based on requested output names - local output_line="" - for out_name in "${output_names[@]}"; do - # Parse output name like "phi_0_min" or "phi_1_max" - if [[ $out_name =~ ^(.*)_([0-9]+)_(min|max)$ ]]; then - local var_name="${BASH_REMATCH[1]}" - local component="${BASH_REMATCH[2]}" - local min_or_max="${BASH_REMATCH[3]}" - - # Get the value - if [ "$min_or_max" = "min" ]; then - local value="${min_components[$component]}" - else - local value="${max_components[$component]}" - fi - - # Remove trailing comma if present - value="${value%,}" - - output_line+="$value " - fi - done - - # Clean up temp files - rm -f temp_min.txt temp_max.txt - - echo "$output_line" - return 0 -} - -# Function to get the last plotfile in a directory -function get_last_plotfile { - local dir=${1:-.} - - if [ ! -d "$dir" ]; then - echo "" - return - fi - - echo "Searching for plotfiles in: $dir" >&2 - - # Find all plotfile directories, sorted by name (which sorts numerically for this format) - local plotfiles=$(find "$dir" -maxdepth 1 -type d -name "plt*" | sort -V) - - # Also check output subdirectory if it exists - if [ -d "$dir/output" ]; then - local output_plotfiles=$(find "$dir/output" -maxdepth 1 -type d -name "plt*" | sort -V) - plotfiles="$plotfiles $output_plotfiles" - fi - - echo "Found plotfiles: $plotfiles" >&2 - - if [ -z "$plotfiles" ]; then - echo "" - return - fi - - # Find the latest plotfile with a complete Header - local last_plotfile="" - for pltFile in $plotfiles; do - echo "Checking plotfile: $pltFile" >&2 - if [ -f "${pltFile}/Header" ]; then - last_plotfile=$pltFile - echo "Valid plotfile found: $pltFile" >&2 - else - echo "No Header found in: $pltFile" >&2 - fi - done - - if [ ! -z "$last_plotfile" ]; then - # Extract just the plotfile name relative to the search directory - local plotfile_name=$(basename "$last_plotfile") - echo "Returning plotfile: $plotfile_name" >&2 - echo "$plotfile_name" - else - echo "No valid plotfiles found" >&2 - echo "" - fi -} - -# Add debug output to run_single_simulation function -function run_single_simulation { - local run_counter=$1 - local param_values=("${@:2}") - - # Debug parameter passing - echo "DEBUG: run_counter=$run_counter" >&2 - echo "DEBUG: param_values=(${param_values[@]})" >&2 - echo "DEBUG: PARAM_NAMES=(${PARAM_NAMES[@]})" >&2 - echo "DEBUG: Length of PARAM_NAMES: ${#PARAM_NAMES[@]}" >&2 - echo "DEBUG: Length of param_values: ${#param_values[@]}" >&2 - - # Create subdirectory for this run - local run_dir="run_$(printf "%04d" $run_counter)" - echo "Creating directory: $run_dir" >&2 - mkdir -p "$run_dir" - - # Change to run directory - cd "$run_dir" - echo "Changed to directory: $(pwd)" >&2 - - # Build command line arguments - local cmd_args="" - if [ ${#PARAM_NAMES[@]} -gt 0 ]; then - echo "DEBUG: Building command arguments..." >&2 - for i in "${!PARAM_NAMES[@]}"; do - if [ $i -lt ${#param_values[@]} ]; then - local arg="${PARAM_NAMES[$i]}=${param_values[$i]}" - cmd_args+="$arg " - echo "DEBUG: Added argument $i: $arg" >&2 - else - echo "DEBUG: Skipping index $i (out of range)" >&2 - fi - done - else - echo "DEBUG: PARAM_NAMES array is empty!" >&2 - fi - - echo "DEBUG: Final cmd_args='$cmd_args'" >&2 - - # Build the full command - local full_command="../$EXE ../$INPUTS $cmd_args" - echo "Running: $full_command" >&2 - - # Run the command and capture output - $full_command > simulation.log 2>&1 - local exit_code=$? - - echo "Simulation exit code: $exit_code" >&2 - - # Check if plotfiles were created (more reliable than exit code for AMReX) - local plotfiles_created=$(find . -maxdepth 1 -type d -name "plt*" | wc -l) - echo "Number of plotfiles created: $plotfiles_created" >&2 - - if [ $plotfiles_created -gt 0 ]; then - echo "Simulation succeeded - plotfiles created" >&2 - echo "Plotfiles found:" >&2 - ls -la plt*/ >&2 - cd .. - # ONLY output the directory name to stdout for capture - echo "$run_dir" - return 0 - else - echo "Simulation failed - no plotfiles created" >&2 - echo "Simulation log:" >&2 - cat simulation.log >&2 - cd .. - return 1 - fi -} - -# Updated process function that uses datalog header automatically -function process_single_simulation_datalog { - local run_dir=$1 - local run_counter=$2 - - echo "Processing simulation $run_counter using datalog approach" >&2 - - if [ ! -d "$run_dir" ]; then - echo "Error: Run directory $run_dir not found" >&2 - return 1 - fi - - # Extract all data from datalog (header parsing handled inside) - local result=$(extract_data_from_datalog "$run_dir" $run_counter) - local extract_success=$? - - if [ $extract_success -eq 0 ]; then - echo "Extracted result: '$result'" >&2 - echo "$result" - return 0 - else - echo "Error: Failed to extract data from datalog in $run_dir" >&2 - return 1 - fi -} - -# Function to process plotfile using combined Header/Cell_H approach -function process_single_plotfile { - local run_dir=$1 - local run_counter=$2 - - echo "Processing plotfile in $run_dir" >&2 - - # Get the last plotfile generated - local plotfile=$(get_last_plotfile "$run_dir") - - echo "Found plotfile: '$plotfile'" >&2 - - if [ -z "$plotfile" ]; then - echo "Error: No plotfile found in $run_dir" >&2 - return 1 - fi - - local plotfile_path="$run_dir/$plotfile" - - # Check required files exist - if [ ! -f "$plotfile_path/Header" ]; then - echo "Error: Header not found at $plotfile_path/Header" >&2 - return 1 - fi - - if [ ! -f "$plotfile_path/Level_${LEVEL}/Cell_H" ]; then - echo "Error: Cell_H not found at $plotfile_path/Level_${LEVEL}/Cell_H" >&2 - return 1 - fi - - echo "Using plotfile path: $plotfile_path" >&2 - - # Generate outnames.txt from combined Header/Cell_H if it doesn't exist - if [ ! -f "outnames.txt" ]; then - echo "Generating outnames.txt from Header variables + Cell_H components..." >&2 - generate_outnames_from_combined_sources "$plotfile_path" "$LEVEL" - if [ -f "outnames.txt" ]; then - echo "Created outnames.txt:" >&2 - cat outnames.txt >&2 - else - echo "Error: Failed to create outnames.txt" >&2 - return 1 - fi - fi - - # Extract min/max values - local result=$(extract_minmax_from_cell_header "$plotfile_path" "$LEVEL" "$TARGET_I" "$TARGET_J" "$TARGET_K" "outnames.txt") - - if [ $? -eq 0 ]; then - echo "Extracted result: '$result'" >&2 - echo "$result" - return 0 - else - echo "Error: Failed to extract data from $run_dir" >&2 - return 1 - fi -} - -# Updated initialize_output_file function -function initialize_output_file { - local output_file=$1 - - echo "Initializing output file: $output_file" >&2 - > "$output_file" - - if [ "$INCLUDE_HEADER" = true ] && [ -f "outnames.txt" ]; then - readarray -t output_names < outnames.txt - echo "# ${output_names[@]}" >> "$output_file" - echo "Added header to $output_file" >&2 - else - echo "Created headerless output file: $output_file" >&2 - fi -} diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp deleted file mode 100644 index d4e69e3e..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp +++ /dev/null @@ -1,320 +0,0 @@ -/* - * A simplified single file version of the HeatEquation_EX0_C exmaple. - * This code is designed to be used with Demo_Tutorial.rst. - * - */ - - -#include -#include -#include - -int main (int argc, char* argv[]) -{ - amrex::Initialize(argc,argv); - { - - // ********************************** - // DECLARE SIMULATION PARAMETERS - // ********************************** - - // number of cells on each side of the domain - int n_cell; - - // size of each box (or grid) - int max_grid_size; - - // total steps in simulation - int nsteps; - - // how often to write a plotfile - int plot_int; - - // time step - amrex::Real dt; - - // ********************************** - // DECLARE PHYSICS PARAMETERS - // ********************************** - - // diffusion coefficient for heat equation - amrex::Real diffusion_coeff; - - // amplitude of initial temperature profile - amrex::Real init_amplitude; - - // width parameter controlling spread of initial profile (variance, not std dev) - amrex::Real init_width; - - // ********************************** - // DECLARE DATALOG PARAMETERS - // ********************************** - const int datwidth = 14; - const int datprecision = 6; - const int timeprecision = 13; - int datalog_int = -1; // Interval for regular output (<=0 means no regular output) - bool datalog_final = true; // Write datalog at final step - - // ********************************** - // READ PARAMETER VALUES FROM INPUT DATA - // ********************************** - // inputs parameters - { - // ParmParse is way of reading inputs from the inputs file - // pp.get means we require the inputs file to have it - // pp.query means we optionally need the inputs file to have it - but we must supply a default here - amrex::ParmParse pp; - - // We need to get n_cell from the inputs file - this is the number of cells on each side of - // a square (or cubic) domain. - pp.get("n_cell",n_cell); - - // The domain is broken into boxes of size max_grid_size - pp.get("max_grid_size",max_grid_size); - - // Default nsteps to 10, allow us to set it to something else in the inputs file - nsteps = 10; - pp.query("nsteps",nsteps); - - // Default plot_int to -1, allow us to set it to something else in the inputs file - // If plot_int < 0 then no plot files will be written - plot_int = -1; - pp.query("plot_int",plot_int); - - // Default datalog_int to -1, allow us to set it to something else in the inputs file - // If datalog_int < 0 then no plot files will be written - datalog_int = -1; - pp.query("datalog_int",datalog_int); - - // time step - pp.get("dt",dt); - - // ********************************** - // READ PHYSICS PARAMETERS - // ********************************** - - // Diffusion coefficient - controls how fast heat spreads - diffusion_coeff = 1.0; - pp.query("diffusion_coeff", diffusion_coeff); // Note: input name is "diffusion" but variable is "diffusion_coeff" - - // Initial temperature amplitude - init_amplitude = 1.0; - pp.query("init_amplitude", init_amplitude); - - // Width parameter - this is the variance (width²), not standard deviation - // Smaller values = more concentrated, larger values = more spread out - init_width = 0.01; // Note: 0.01 to match your original rsquared/0.01 - pp.query("init_width", init_width); - } - - // ********************************** - // DEFINE SIMULATION SETUP AND GEOMETRY - // ********************************** - - // make BoxArray and Geometry - // ba will contain a list of boxes that cover the domain - // geom contains information such as the physical domain size, - // number of points in the domain, and periodicity - amrex::BoxArray ba; - amrex::Geometry geom; - - // define lower and upper indices - amrex::IntVect dom_lo(0,0,0); - amrex::IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); - - // Make a single box that is the entire domain - amrex::Box domain(dom_lo, dom_hi); - - // Initialize the boxarray "ba" from the single box "domain" - ba.define(domain); - - // Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction - ba.maxSize(max_grid_size); - - // This defines the physical box, [0,1] in each direction. - amrex::RealBox real_box({ 0., 0., 0.}, - { 1., 1., 1.}); - - // periodic in all direction - amrex::Array is_periodic{1,1,1}; - - // This defines a Geometry object - geom.define(domain, real_box, amrex::CoordSys::cartesian, is_periodic); - - // extract dx from the geometry object - amrex::GpuArray dx = geom.CellSizeArray(); - - // Nghost = number of ghost cells for each array - int Nghost = 1; - - // Ncomp = number of components for each array - int Ncomp = 1; - - // How Boxes are distrubuted among MPI processes - amrex::DistributionMapping dm(ba); - - // we allocate two phi multifabs; one will store the old state, the other the new. - amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); - amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); - - // time = starting time in the simulation - amrex::Real time = 0.0; - - // ********************************** - // INITIALIZE DATA LOOP - // ********************************** - - // loop over boxes - for (amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi) - { - const amrex::Box& bx = mfi.validbox(); - - const amrex::Array4& phiOld = phi_old.array(mfi); - - // ********************************** - // SET INITIAL TEMPERATURE PROFILE - // ********************************** - // Formula: T = 1 + amplitude * exp(-r^2 / width^2) - // where r is distance from center (0.5, 0.5, 0.5) - // - // Parameters: - // - amplitude: controls peak temperature above baseline (1.0) - // - width: controls spread of initial hot spot - // - smaller width = more concentrated - // - larger width = more spread out - -amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) - { - // Calculate physical coordinates of cell center - amrex::Real x = (i+0.5) * dx[0]; - amrex::Real y = (j+0.5) * dx[1]; - amrex::Real z = (k+0.5) * dx[2]; - - // Calculate squared distance from domain center (0.5, 0.5, 0.5) - // Divide by init_width (which is the variance, not standard deviation) - amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; - - // Set initial temperature profile - phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); - }); - } - - // ********************************** - // WRITE DATALOG FILE - // ********************************** - if (amrex::ParallelDescriptor::IOProcessor() && (datalog_int>0 || datalog_final)) { - std::ofstream datalog("datalog.txt"); // truncate mode to start fresh - datalog << "#" << std::setw(datwidth-1) << " time"; - datalog << std::setw(datwidth) << " max_temp"; - datalog << std::setw(datwidth) << " std_temp"; - datalog << std::setw(datwidth) << " final_step"; - datalog << std::endl; - datalog.close(); - } - - // ********************************** - // WRITE INITIAL PLOT FILE - // ********************************** - - // Write a plotfile of the initial data if plot_int > 0 - if (plot_int > 0) - { - int step = 0; - const std::string& pltfile = amrex::Concatenate("plt",step,5); - WriteSingleLevelPlotfile(pltfile, phi_old, {"phi"}, geom, time, 0); - } - - - // ********************************** - // MAIN TIME EVOLUTION LOOP - // ********************************** - - for (int step = 1; step <= nsteps; ++step) - { - // fill periodic ghost cells - phi_old.FillBoundary(geom.periodicity()); - - // new_phi = old_phi + dt * Laplacian(old_phi) - // loop over boxes - for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ) - { - const amrex::Box& bx = mfi.validbox(); - - const amrex::Array4& phiOld = phi_old.array(mfi); - const amrex::Array4& phiNew = phi_new.array(mfi); - - // advance the data by dt using heat equation with diffusion coefficient - amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) - { - // Calculate the discrete Laplacian using finite differences - amrex::Real laplacian = - (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) - +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) - +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]); - - // Apply heat equation using diffusion_coeff - matches Python version - phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; - }); - } - - // ********************************** - // INCREMENT - // ********************************** - - // update time - time = time + dt; - - // copy new solution into old solution - amrex::MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); - - // Tell the I/O Processor to write out which step we're doing - amrex::Print() << "Advanced step " << step << "\n"; - - // ********************************** - // WRITE DATALOG AT GIVEN INTERVAL - // ********************************** - - // Check if we should write datalog - bool write_datalog = false; - if (datalog_final && step == nsteps) { - write_datalog = true; // Write final step - } else if (datalog_int > 0 && step % datalog_int == 0) { - write_datalog = true; // Write every datalog_int steps - } - - if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { - std::ofstream datalog("datalog.txt", std::ios::app); - - // Calculate temperature statistics - amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); - amrex::Real max_temperature = phi_new.max(0); - amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; - amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; - - datalog << std::setw(datwidth) << std::setprecision(timeprecision) << time; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << max_temperature; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << std_temperature; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << step; - datalog << std::endl; - - datalog.close(); - } - - // ********************************** - // WRITE PLOTFILE AT GIVEN INTERVAL - // ********************************** - - // Write a plotfile of the current data (plot_int was defined in the inputs file) - if (plot_int > 0 && step%plot_int == 0) - { - const std::string& pltfile = amrex::Concatenate("plt",step,5); - WriteSingleLevelPlotfile(pltfile, phi_new, {"phi"}, geom, time, step); - } - } - - } - amrex::Finalize(); - return 0; -} - - diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x deleted file mode 100755 index 7750cc79..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/model.x +++ /dev/null @@ -1,163 +0,0 @@ -#!/bin/bash -# model.x - wrapper script that extracts min/max from plotfile headers - -# Source the functions from external file -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/functions.sh" - -# CONFIGURABLE VARIABLES -EXE="main3d.gnu.ex" # Executable -INPUTS="inputs" # AMReX inputs file -LEVEL=0 # Default level -TARGET_I=16 # Target i coordinate (middle of 32^3 grid) -TARGET_J=16 # Target j coordinate -TARGET_K=16 # Target k coordinate - -# Create parameter names file -#cat > pnames.txt << EOF -#input_1 -#input_2 -#input_3 -#input_4 -#input_5 -#EOF - -# Create parameter marginal PC file for uncertainty quantification -cat > param_margpc.txt << EOF -1.0 0.25 -1.0 0.25 -0.01 0.0025 -EOF - -# PC configuration for uncertainty quantification -PC_TYPE=HG # Hermite-Gaussian PC -INPC_ORDER=1 - -# Output file header -INCLUDE_HEADER=true # Set to false for pure numbers - -# Get positional arguments -INPUT_FILE="$1" -OUTPUT_FILE="$2" - -# Validate arguments -if [ -z "$INPUT_FILE" ] || [ -z "$OUTPUT_FILE" ]; then - echo "Usage: $0 " - echo "Example: $0 ptrain.txt ytrain.txt" - echo "" - echo "Heat Equation Test Parameters:" - echo " diffusion_coeff: thermal diffusion coefficient" - echo " init_amplitude: amplitude of initial temperature profile" - echo " init_width: width parameter (variance) of initial profile" - echo "" - echo "Required files:" - echo " $EXE: executable" - echo " $INPUTS: AMReX inputs file" - exit 1 -fi - -if [ ! -f "$INPUT_FILE" ]; then - echo "Error: Input file $INPUT_FILE not found!" - exit 1 -fi - -if [ ! -f "$EXE" ]; then - echo "Error: Executable $EXE not found!" - exit 1 -fi - -if [ ! -f "$INPUTS" ]; then - echo "Error: Inputs file $INPUTS not found!" - exit 1 -fi - -# Check if parameter names file exists -if [ ! -f "pnames.txt" ]; then - echo "Warning: pnames.txt not found! Using default parameter names." -fi - -# Read parameter names if file exists -if [ -f "pnames.txt" ]; then - readarray -t PARAM_NAMES < pnames.txt - echo "Parameter names: ${PARAM_NAMES[@]}" -fi - -echo "Heat Equation Test Configuration:" -echo "Target coordinates: ($TARGET_I,$TARGET_J,$TARGET_K)" -echo "Using executable: $EXE" -echo "Using inputs file: $INPUTS" -echo "Processing level: $LEVEL" - -# Function to process all simulations with better debugging -function process_all_simulations { - local input_file=$1 - local output_file=$2 - - local run_counter=0 - local output_initialized=false - - # Read input file line by line - while IFS= read -r line; do - # Skip empty lines and comments - [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue - - # Parse parameter values - local param_values=($(echo "$line")) - - if [ ${#param_values[@]} -eq 0 ]; then - echo "Warning: Empty parameter line at run $run_counter, skipping" - continue - fi - - echo "=== Processing run $run_counter ===" - echo "Parameters: ${param_values[@]}" - - # Run the simulation - echo "Step 1: Running simulation..." - local run_dir=$(run_single_simulation $run_counter "${param_values[@]}") - local sim_success=$? - - echo "Simulation result: exit_code=$sim_success, run_dir='$run_dir'" - - if [ $sim_success -eq 0 ] && [ -d "$run_dir" ]; then - echo "Step 2: Processing plotfile..." - # NEW - using datalog - local result=$(process_single_simulation_datalog "$run_dir" $run_counter) - #local result=$(process_single_plotfile "$run_dir" $run_counter) - local process_success=$? - - echo "Process result: exit_code=$process_success, result='$result'" - - if [ $process_success -eq 0 ]; then - # Initialize output file on first successful run - if [ "$output_initialized" = false ]; then - initialize_output_file "$output_file" - output_initialized=true - fi - - # Write result to output file - echo "$result" >> "$output_file" - echo "✓ Run $run_counter completed successfully" - else - echo "✗ Failed to process plotfile for run $run_counter" - fi - else - echo "✗ Failed to run simulation $run_counter" - fi - - ((run_counter++)) - echo "" - - done < "$input_file" - - echo "Completed processing $run_counter runs" - if [ -f "$output_file" ]; then - echo "Output written to $output_file:" - cat "$output_file" - else - echo "No output file created - all runs failed" - fi -} - -# Call the main processing function -process_all_simulations "$INPUT_FILE" "$OUTPUT_FILE" diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXBaseModel.py deleted file mode 100644 index 251c850c..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXBaseModel.py +++ /dev/null @@ -1,239 +0,0 @@ -class AMReXModelBase(ModelWrapperFcn): - # Class-level field definitions - _field_info_class = None - _param_fields = [] - _output_fields = [] - _spatial_domain_bounds = None # Spatial domain bounds (separate from parameter bounds) - - def __init__(self, model=None, **kwargs): - # Create modelpar from existing parameter information - modelpar = self._create_modelpar() - - # Setup field info container - self.field_info = self._create_field_info() - - # Setup convenience lists - self.param_names = [f[1] for f in self._param_fields] - self.output_names = [f[1] for f in self._output_fields] - - # Extract parameter bounds and create domain array for Function - param_domain = self._extract_param_domain() - - # Determine dimensions - ndim = kwargs.get('ndim', len(self.param_names)) - outdim = kwargs.get('outdim', len(self.output_names)) - - # Create model function wrapper - if model is None: - model_func = lambda params: self._run_simulation(params) - else: - model_func = model - - # Initialize Function with model - super().__init__( - model_func, - ndim, - modelpar=modelpar, - name=kwargs.get('name', 'AMReXModel') - ) - - # Set the parameter domain using Function's method - if param_domain is not None and len(param_domain) > 0: - self.setDimDom(domain=param_domain) - - # Set output dimension - self.outdim = outdim - - # Setup spatial domain bounds (yt-style) - separate from parameter bounds - if self._spatial_domain_bounds: - self.domain_left_edge = self._spatial_domain_bounds[0] - self.domain_right_edge = self._spatial_domain_bounds[1] - self.domain_dimensions = (self._spatial_domain_bounds[2] - if len(self._spatial_domain_bounds) > 2 else None) - - # Initialize AMReX if needed - self.xp = load_cupy() - if not amr.initialized(): - amr.initialize([]) - - def _create_field_info(self): - """Create yt-style field info container""" - field_info = {} - - for field_tuple in self._param_fields: - field_info[field_tuple] = self._get_field_info(field_tuple) - - for field_tuple in self._output_fields: - field_info[field_tuple] = self._get_field_info(field_tuple) - - return field_info - - def _extract_param_domain(self): - """Extract parameter bounds into Function-compatible domain array""" - domain_list = [] - - for field_tuple in self._param_fields: - info = self._get_field_info(field_tuple) - if 'bounds' in info: - domain_list.append(info['bounds']) - else: - # Use Function's default domain size - dmax = getattr(self, 'dmax', 10.0) - domain_list.append([-dmax, dmax]) - - if domain_list: - return np.array(domain_list) - return None - - def forward(self, x): - """ - PyTorch-compatible forward method for inference. - - Args: - x: torch.Tensor or np.ndarray - Returns: - torch.Tensor if input is tensor, else np.ndarray - """ - # Check if input is PyTorch tensor - is_torch = False - if hasattr(x, 'detach'): # Duck typing for torch.Tensor - is_torch = True - import torch - x_np = x.detach().cpu().numpy() - else: - x_np = x - - # Run simulation using existing logic - outputs = self._run_simulation(x_np) - - # Convert back to torch if needed - if is_torch: - return torch.from_numpy(outputs).to(x.device) - return outputs - - def __call__(self, x): - """Function interface - routes through forward() for consistency""" - self.checkDim(x) - if hasattr(self, 'domain') and self.domain is not None: - self.checkDomain(x) - return self.forward(x) - - def _run_simulation(self, params): - """Core simulation logic - override in subclasses""" - if params.ndim == 1: - params = params.reshape(1, -1) - - n_samples = params.shape[0] - outputs = np.zeros((n_samples, self.outdim)) - - # Default implementation using evolve/postprocess if available - if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): - for i in range(n_samples): - multifab, varnames, geom = self.evolve(params[i, :]) - outputs[i, :] = self.postprocess(multifab, varnames, geom) - else: - # Must be overridden in subclass - raise NotImplementedError("Must implement _run_simulation or evolve/postprocess") - - return outputs - - @property - def field_list(self): - """All available fields""" - return list(self.field_info.keys()) - - def _get_field_info(self, field_tuple): - """Override to provide field metadata""" - return {} - - def get_param_info(self, param_name): - """Get info for a specific parameter""" - for field_tuple in self._param_fields: - if field_tuple[1] == param_name: - return self._get_field_info(field_tuple) - return {} - - def get_output_info(self, output_name): - """Get info for a specific output""" - for field_tuple in self._output_fields: - if field_tuple[1] == output_name: - return self._get_field_info(field_tuple) - return {} - - def _create_modelpar(self): - """Create modelpar dictionary with statistical properties""" - modelpar = { - 'param_info': {}, - 'output_info': {}, - 'param_names': [], - 'output_names': [], - 'defaults': {}, - 'bounds': {}, - 'units': {}, - 'mean': [], - 'std': [], - 'distribution': [], # 'normal', 'uniform', 'lognormal', etc. - 'pc_type': 'HG', # Hermite-Gaussian by default - } - - # Extract parameter information including statistical properties - for field_tuple in self._param_fields: - field_type, field_name = field_tuple - info = self._get_field_info(field_tuple) - - modelpar['param_info'][field_name] = info - modelpar['param_names'].append(field_name) - - # Extract statistical properties - if 'mean' in info: - modelpar['mean'].append(info['mean']) - elif 'default' in info: - modelpar['mean'].append(info['default']) - else: - # Use center of bounds if available - if 'bounds' in info: - modelpar['mean'].append(np.mean(info['bounds'])) - else: - modelpar['mean'].append(0.0) - - if 'std' in info: - modelpar['std'].append(info['std']) - else: - # Default to 10% of mean or range - if 'bounds' in info: - # Use range/6 as rough std (99.7% within bounds) - modelpar['std'].append((info['bounds'][1] - info['bounds'][0])/6.0) - else: - modelpar['std'].append(abs(modelpar['mean'][-1]) * 0.1) - - # Store other properties - if 'bounds' in info: - modelpar['bounds'][field_name] = info['bounds'] - if 'units' in info: - modelpar['units'][field_name] = info['units'] - if 'distribution' in info: - modelpar['distribution'].append(info['distribution']) - else: - modelpar['distribution'].append('normal') # default - - # Convert to numpy arrays for easier manipulation - modelpar['mean'] = np.array(modelpar['mean']) - modelpar['std'] = np.array(modelpar['std']) - - # Add output information - for field_tuple in self._output_fields: - field_type, field_name = field_tuple - info = self._get_field_info(field_tuple) - modelpar['output_info'][field_name] = info - modelpar['output_names'].append(field_name) - - return modelpar - - def write_param_marginals(self, filename='param_margpc.txt'): - """Write parameter marginals file for PC analysis""" - with open(filename, 'w') as f: - for i, name in enumerate(self.modelpar['param_names']): - mean = self.modelpar['mean'][i] - std = self.modelpar['std'][i] - f.write(f"{mean} {std}\n") - print(f"Wrote parameter marginals to {filename}") diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXTorchModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXTorchModel.py deleted file mode 100644 index 498949a5..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/AMReXTorchModel.py +++ /dev/null @@ -1,32 +0,0 @@ -class AMReXTorchModel(AMReXModelBase, nn.Module): - """Full PyTorch nn.Module compatibility with Function parameter handling""" - - def __init__(self, **kwargs): - AMReXModelBase.__init__(self, **kwargs) - nn.Module.__init__(self) - - # Register any learnable parameters if needed - self.register_parameter('scale', nn.Parameter(torch.ones(1))) - self.register_parameter('bias', nn.Parameter(torch.zeros(1))) - - def forward(self, x): - """Enhanced forward with learnable parameters""" - # Get base simulation results - outputs = AMReXModelBase.forward(self, x) - - # Apply learnable transformations if using PyTorch - if isinstance(outputs, torch.Tensor): - outputs = outputs * self.scale + self.bias - - return outputs - - def train_step(self, inputs, targets, optimizer, loss_fn): - """Example training step for PyTorch workflows""" - optimizer.zero_grad() - outputs = self.forward(inputs) - loss = loss_fn(outputs, targets) - loss.backward() - optimizer.step() - return loss.item() - - diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py deleted file mode 100755 index d19ce1f0..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/HeatEquationModel.py +++ /dev/null @@ -1,396 +0,0 @@ -#!/usr/bin/env python3 -import numpy as np -from typing import Tuple, Optional -import amrex.space3d as amr -from AMReXModelBase import AMReXModelBase, load_cupy - - -class HeatEquationModel(AMReXModelBase): - """ - Heat equation simulation model with full statistical properties - and AMReX integration. - """ - - # Define parameter fields with proper naming convention - _param_fields = [ - ('param', 'diffusion_coefficient'), - ('param', 'initial_amplitude'), - ('param', 'initial_width'), - ] - - # Define output fields - _output_fields = [ - ('output', 'max_temperature'), - ('output', 'mean_temperature'), - ('output', 'std_temperature'), - ('output', 'total_energy'), - ('output', 'center_temperature'), - ] - - # Spatial domain bounds (3D heat equation domain) - _spatial_domain_bounds = [ - np.array([0.0, 0.0, 0.0]), # left edge - np.array([1.0, 1.0, 1.0]), # right edge - np.array([32, 32, 32]) # default grid dimensions - ] - - def __init__(self, n_cell: int = 32, max_grid_size: int = 16, - nsteps: int = 1000, plot_int: int = 100, - dt: float = 1e-5, use_parmparse: bool = False, **kwargs): - """ - Initialize heat equation model. - - Parameters: - ----------- - n_cell : int - Number of grid cells in each dimension - max_grid_size : int - Maximum grid size for AMR - nsteps : int - Number of time steps - plot_int : int - Plot interval (for output control) - dt : float - Time step size - use_parmparse : bool - Whether to use AMReX ParmParse for input - """ - # Store simulation parameters - if use_parmparse: - from main import parse_inputs - params = parse_inputs() - self.n_cell = params['n_cell'] - self.max_grid_size = params['max_grid_size'] - self.nsteps = params['nsteps'] - self.plot_int = params['plot_int'] - self.dt = params['dt'] - else: - self.n_cell = n_cell - self.max_grid_size = max_grid_size - self.nsteps = nsteps - self.plot_int = plot_int - self.dt = dt - - # Update spatial domain dimensions based on n_cell - if self.n_cell != 32: - self._spatial_domain_bounds[2] = np.array([self.n_cell, self.n_cell, self.n_cell]) - - # Initialize base class - super().__init__(**kwargs) - - # Store physical constants - self.thermal_conductivity = 1.0 # W/(m·K) - self.specific_heat = 1.0 # J/(kg·K) - self.density = 1.0 # kg/m³ - - def _get_field_info(self, field_tuple): - """ - Provide detailed field metadata with statistical properties. - """ - field_type, field_name = field_tuple - - field_info_dict = { - ('param', 'diffusion_coefficient'): { - 'units': 'm**2/s', - 'display_name': 'Thermal Diffusivity', - 'description': 'α = k/(ρ·cp) - thermal diffusivity coefficient', - 'mean': 1.0, - 'std': 0.3, - 'bounds': [0.1, 5.0], - 'distribution': 'lognormal', # Physical parameter, must be positive - 'log_scale': True, - 'default': 1.0, - 'physical_range': [1e-7, 1e-3], # Realistic range for materials (m²/s) - 'material_examples': { - 'air': 2.2e-5, - 'water': 1.4e-7, - 'steel': 1.2e-5, - 'copper': 1.1e-4, - } - }, - ('param', 'initial_amplitude'): { - 'units': 'K', - 'display_name': 'Initial Temperature Amplitude', - 'description': 'Peak temperature of initial Gaussian distribution', - 'mean': 1.0, - 'std': 0.2, - 'bounds': [0.1, 3.0], - 'distribution': 'normal', - 'log_scale': False, - 'default': 1.0, - 'normalized': True, # Values are normalized to reference temperature - }, - ('param', 'initial_width'): { - 'units': 'm', - 'display_name': 'Initial Distribution Width', - 'description': 'Standard deviation of initial Gaussian temperature field', - 'mean': 0.01, - 'std': 0.003, - 'bounds': [0.001, 0.1], - 'distribution': 'lognormal', # Width must be positive - 'log_scale': True, - 'default': 0.01, - 'relative_to_domain': True, # As fraction of domain size - }, - ('output', 'max_temperature'): { - 'units': 'K', - 'display_name': 'Maximum Temperature', - 'description': 'Maximum temperature in the domain', - 'valid_range': [0.0, np.inf], - 'expected_range': [0.1, 3.0], # Typical range for normalized runs - 'monotonic': 'decreasing', # Decreases with time due to diffusion - }, - ('output', 'mean_temperature'): { - 'units': 'K', - 'display_name': 'Mean Temperature', - 'description': 'Spatial average of temperature field', - 'valid_range': [0.0, np.inf], - 'expected_range': [0.0, 1.0], - 'conserved': True, # Should be conserved in isolated system - }, - ('output', 'std_temperature'): { - 'units': 'K', - 'display_name': 'Temperature Standard Deviation', - 'description': 'Spatial standard deviation of temperature', - 'valid_range': [0.0, np.inf], - 'expected_range': [0.0, 1.0], - 'monotonic': 'decreasing', # Decreases as field homogenizes - }, - ('output', 'total_energy'): { - 'units': 'J', - 'display_name': 'Total Thermal Energy', - 'description': 'Integrated thermal energy in domain', - 'valid_range': [0.0, np.inf], - 'conserved': True, # Conserved quantity - 'scaling': 'extensive', # Scales with system size - }, - ('output', 'center_temperature'): { - 'units': 'K', - 'display_name': 'Center Temperature', - 'description': 'Temperature at domain center', - 'valid_range': [0.0, np.inf], - 'expected_range': [0.0, 3.0], - 'probe_location': [0.5, 0.5, 0.5], # Normalized coordinates - }, - } - - return field_info_dict.get(field_tuple, {}) - - def evolve(self, param_set: np.ndarray) -> Tuple[amr.MultiFab, amr.Vector_string, amr.Geometry]: - """ - Run heat equation simulation with given parameters. - - Parameters: - ----------- - param_set : np.ndarray - [diffusion_coefficient, initial_amplitude, initial_width] - - Returns: - -------- - tuple : (phi_new, varnames, geom) - MultiFab with solution, variable names, and geometry - """ - from main import main # Import simulation driver - - if len(param_set) != len(self.param_names): - raise ValueError(f"Expected {len(self.param_names)} parameters, got {len(param_set)}") - - # Extract parameters with proper names - diffusion_coeff = float(param_set[0]) - init_amplitude = float(param_set[1]) - init_width = float(param_set[2]) - - # Validate parameters against bounds - for i, (param_val, param_name) in enumerate(zip(param_set, self.param_names)): - info = self.get_param_info(param_name) - if 'bounds' in info: - if param_val < info['bounds'][0] or param_val > info['bounds'][1]: - print(f"Warning: {param_name}={param_val} outside bounds {info['bounds']}") - - # Run simulation - phi_new, geom = main( - diffusion_coeff=diffusion_coeff, - init_amplitude=init_amplitude, - init_width=init_width, - n_cell=self.n_cell, - max_grid_size=self.max_grid_size, - nsteps=self.nsteps, - plot_int=self.plot_int, - dt=self.dt, - plot_files_output=False, - verbose=0 - ) - - varnames = amr.Vector_string(['temperature']) - return phi_new, varnames, geom - - def postprocess(self, multifab: amr.MultiFab, - varnames: Optional[amr.Vector_string] = None, - geom: Optional[amr.Geometry] = None) -> np.ndarray: - """ - Extract output quantities from simulation results. - - Parameters: - ----------- - multifab : amr.MultiFab - Simulation data (temperature field) - varnames : amr.Vector_string or None - Variable names - geom : amr.Geometry or None - Domain geometry - - Returns: - -------- - np.ndarray - Output quantities [max, mean, std, total_energy, center_value] - """ - xp = load_cupy() - - # Get basic statistics - max_val = multifab.max(comp=0, local=False) - sum_val = multifab.sum(comp=0, local=False) - total_cells = multifab.box_array().numPts - mean_val = sum_val / total_cells - - # Calculate standard deviation using L2 norm - l2_norm = multifab.norm2(0) - sum_sq = l2_norm**2 - variance = (sum_sq / total_cells) - mean_val**2 - std_val = np.sqrt(max(0, variance)) - - # Calculate total energy (normalized by cell volume if geometry available) - if geom is not None: - dx = geom.data().CellSize() - cell_volume = dx[0] * dx[1] * dx[2] - total_energy = sum_val * cell_volume * self.density * self.specific_heat - else: - total_energy = sum_val # Dimensionless if no geometry - - # Get temperature at domain center - center_val = 0.0 - if geom is not None: - dx = geom.data().CellSize() - prob_lo = geom.data().ProbLo() - prob_hi = geom.data().ProbHi() - - # Calculate center coordinates - center_coords = [(prob_lo[i] + prob_hi[i]) / 2.0 for i in range(3)] - - # Find the cell index closest to center - center_indices = [int((center_coords[i] - prob_lo[i]) / dx[i]) for i in range(3)] - - # Get value at center - try: - for mfi in multifab: - bx = mfi.validbox() - if all(center_indices[i] >= bx.small_end[i] and - center_indices[i] <= bx.big_end[i] for i in range(3)): - - state_arr = xp.array(multifab.array(mfi), copy=False) - # Convert global to local indices - local_indices = [center_indices[i] - bx.small_end[i] for i in range(3)] - center_val = float(state_arr[0, local_indices[2], - local_indices[1], local_indices[0]]) - break - except (IndexError, AttributeError) as e: - print(f"Warning: Could not access center cell: {e}") - center_val = mean_val # Fall back to mean - else: - center_val = mean_val # No geometry, use mean as proxy - - return np.array([max_val, mean_val, std_val, total_energy, center_val]) - - def validate_outputs(self, outputs: np.ndarray) -> bool: - """ - Validate that outputs are physically reasonable. - - Parameters: - ----------- - outputs : np.ndarray - Output values to validate - - Returns: - -------- - bool - True if outputs are valid - """ - if outputs.shape[-1] != len(self.output_names): - return False - - # Check each output against its valid range - for i, output_name in enumerate(self.output_names): - info = self.get_output_info(output_name) - if 'valid_range' in info: - min_val, max_val = info['valid_range'] - if outputs[..., i].min() < min_val or outputs[..., i].max() > max_val: - print(f"Warning: {output_name} outside valid range {info['valid_range']}") - return False - - # Physical consistency checks - max_temp = outputs[..., 0] - mean_temp = outputs[..., 1] - - # Maximum should be >= mean - if np.any(max_temp < mean_temp): - print("Warning: Maximum temperature less than mean") - return False - - return True - - -if __name__ == "__main__": - # Initialize AMReX - amr.initialize([]) - - # Create model with default parameters - model = HeatEquationModel(n_cell=32, nsteps=100, dt=1e-5) - - print("Heat Equation Model Configuration:") - print("=" * 50) - print(f"Parameter fields: {model.param_names}") - print(f"Output fields: {model.output_names}") - print(f"Domain: {model.domain_left_edge} to {model.domain_right_edge}") - print(f"Grid: {model.domain_dimensions}") - - # Show statistical properties - print("\nParameter Statistics:") - print("-" * 50) - for i, param_name in enumerate(model.param_names): - info = model.get_param_info(param_name) - print(f"{param_name}:") - print(f" Mean: {model.modelpar['mean'][i]:.3f} {info.get('units', '')}") - print(f" Std: {model.modelpar['std'][i]:.3f}") - print(f" Bounds: {info.get('bounds', 'Not specified')}") - print(f" Distribution: {info.get('distribution', 'normal')}") - - # Test with different parameter sets - test_params = np.array([ - [1.0, 1.0, 0.01], # baseline - [2.0, 1.5, 0.02], # higher diffusion, higher amplitude - [0.5, 2.0, 0.005] # lower diffusion, higher amplitude, narrower - ]) - - print("\nRunning simulations...") - print("-" * 50) - outputs = model(test_params) - - print("\nResults:") - print("-" * 50) - for i, params in enumerate(test_params): - print(f"Run {i+1}: D={params[0]:.2f}, A={params[1]:.2f}, W={params[2]:.3f}") - for j, output_name in enumerate(model.output_names): - info = model.get_output_info(output_name) - units = info.get('units', '') - print(f" {info.get('display_name', output_name)}: {outputs[i,j]:.4f} {units}") - - # Validate outputs - if model.validate_outputs(outputs): - print("\n✓ All outputs are physically valid") - else: - print("\n✗ Some outputs failed validation") - - # Write parameter marginals for UQ analysis - model.write_param_marginals('heat_equation_marginals.txt') - - # Finalize AMReX - amr.finalize() diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/README.md b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/README.md deleted file mode 100644 index f328b031..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/README.md +++ /dev/null @@ -1 +0,0 @@ -This is an example of an object oriented approach creating a HeatEquationModel object in python that wraps the contents of main.py from the GuidedTutorials/HeatEquation/Source example \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs deleted file mode 100644 index 65a1c23d..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/inputs +++ /dev/null @@ -1,11 +0,0 @@ -n_cell = 32 -max_grid_size = 16 - -nsteps = 10 -plot_int = 100 - -dt = 1.e-5 - -plot_files_output = False - -verbose = 0 \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py deleted file mode 100755 index 03533917..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/main.py +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Copyright 2023 The AMReX Community -# -# This file is part of AMReX. -# -# License: BSD-3-Clause-LBNL -# Authors: Revathi Jambunathan, Edoardo Zoni, Olga Shapoval, David Grote, Axel Huebl - -import amrex.space3d as amr -import numpy as np -from typing import Tuple, Dict, Union - - -def load_cupy(): - """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" - if amr.Config.have_gpu: - try: - import cupy as cp - amr.Print("Note: found and will use cupy") - return cp - except ImportError: - amr.Print("Warning: GPU found but cupy not available! Trying managed memory in numpy...") - import numpy as np - return np - if amr.Config.gpu_backend == "SYCL": - amr.Print("Warning: SYCL GPU backend not yet implemented for Python") - import numpy as np - return np - else: - import numpy as np - amr.Print("Note: found and will use numpy") - return np - - -def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, - plot_int: int = 100, dt: float = 1e-5, plot_files_output: bool = False, - verbose: int = 1, diffusion_coeff: float = 1.0, init_amplitude: float = 1.0, - init_width: float = 0.01) -> Tuple[amr.MultiFab, amr.Geometry]: - """ - Run the heat equation simulation. - The main function, automatically called below if called as a script. - - Returns: - -------- - tuple : (phi_new, geom) - Final state and geometry - """ - # CPU/GPU logic - xp = load_cupy() - - # AMREX_D_DECL means "do the first X of these, where X is the dimensionality of the simulation" - dom_lo = amr.IntVect(*amr.d_decl( 0, 0, 0)) - dom_hi = amr.IntVect(*amr.d_decl(n_cell-1, n_cell-1, n_cell-1)) - - # Make a single box that is the entire domain - domain = amr.Box(dom_lo, dom_hi) - - # Make BoxArray and Geometry: - # ba contains a list of boxes that cover the domain, - # geom contains information such as the physical domain size, - # number of points in the domain, and periodicity - - # Initialize the boxarray "ba" from the single box "domain" - ba = amr.BoxArray(domain) - # Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction - ba.max_size(max_grid_size) - - # This defines the physical box, [0,1] in each direction. - real_box = amr.RealBox([*amr.d_decl( 0., 0., 0.)], [*amr.d_decl( 1., 1., 1.)]) - - # This defines a Geometry object - # periodic in all direction - coord = 0 # Cartesian - is_per = [*amr.d_decl(1,1,1)] # periodicity - geom = amr.Geometry(domain, real_box, coord, is_per); - - # Extract dx from the geometry object - dx = geom.data().CellSize() - - # Nghost = number of ghost cells for each array - Nghost = 1 - - # Ncomp = number of components for each array - Ncomp = 1 - - # How Boxes are distrubuted among MPI processes - dm = amr.DistributionMapping(ba) - - # Allocate two phi multifabs: one will store the old state, the other the new. - phi_old = amr.MultiFab(ba, dm, Ncomp, Nghost) - phi_new = amr.MultiFab(ba, dm, Ncomp, Nghost) - phi_old.set_val(0.) - phi_new.set_val(0.) - - # time = starting time in the simulation - time = 0. - - # Ghost cells - ng = phi_old.n_grow_vect - ngx = ng[0] - ngy = ng[1] - ngz = ng[2] - - # Loop over boxes - for mfi in phi_old: - bx = mfi.validbox() - # phiOld is indexed in reversed order (z,y,x) and indices are local - phiOld = xp.array(phi_old.array(mfi), copy=False) - # set phi = 1 + amplitude * e^(-(r-0.5)^2 / width) - x = (xp.arange(bx.small_end[0],bx.big_end[0]+1,1) + 0.5) * dx[0] - y = (xp.arange(bx.small_end[1],bx.big_end[1]+1,1) + 0.5) * dx[1] - z = (xp.arange(bx.small_end[2],bx.big_end[2]+1,1) + 0.5) * dx[2] - rsquared = ((z[: , xp.newaxis, xp.newaxis] - 0.5)**2 - + (y[xp.newaxis, : , xp.newaxis] - 0.5)**2 - + (x[xp.newaxis, xp.newaxis, : ] - 0.5)**2) / init_width - phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + init_amplitude * xp.exp(-rsquared) - - # Write a plotfile of the initial data if plot_int > 0 and plot_files_output - if plot_int > 0 and plot_files_output: - step = 0 - pltfile = amr.concatenate("plt", step, 5) - varnames = amr.Vector_string(['phi']) - amr.write_single_level_plotfile(pltfile, phi_old, varnames, geom, time, 0) - - for step in range(1, nsteps+1): - # Fill periodic ghost cells - phi_old.fill_boundary(geom.periodicity()) - - # new_phi = old_phi + dt * Laplacian(old_phi) - # Loop over boxes - for mfi in phi_old: - phiOld = xp.array(phi_old.array(mfi), copy=False) - phiNew = xp.array(phi_new.array(mfi), copy=False) - hix = phiOld.shape[3] - hiy = phiOld.shape[2] - hiz = phiOld.shape[1] - - # Heat equation with parameterized diffusion - # Advance the data by dt - phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( - phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + dt * diffusion_coeff * - (( phiOld[:, ngz :-ngz , ngy :-ngy , ngx+1:hix-ngx+1] - -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] - +phiOld[:, ngz :-ngz , ngy :-ngy , ngx-1:hix-ngx-1]) / dx[0]**2 - +( phiOld[:, ngz :-ngz , ngy+1:hiy-ngy+1, ngx :-ngx ] - -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] - +phiOld[:, ngz :-ngz , ngy-1:hiy-ngy-1, ngx :-ngx ]) / dx[1]**2 - +( phiOld[:, ngz+1:hiz-ngz+1, ngy :-ngy , ngx :-ngx ] - -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] - +phiOld[:, ngz-1:hiz-ngz-1, ngy :-ngy , ngx :-ngx ]) / dx[2]**2)) - - # Update time - time = time + dt - - # Copy new solution into old solution - amr.copy_mfab(dst=phi_old, src=phi_new, srccomp=0, dstcomp=0, numcomp=1, nghost=0) - - # Tell the I/O Processor to write out which step we're doing - if(verbose > 0): - amr.Print(f'Advanced step {step}\n') - - # Write a plotfile of the current data (plot_int was defined in the inputs file) - if plot_int > 0 and step%plot_int == 0 and plot_files_output: - pltfile = amr.concatenate("plt", step, 5) - varnames = amr.Vector_string(['phi']) - amr.write_single_level_plotfile(pltfile, phi_new, varnames, geom, time, step) - - return phi_new, geom - - -def parse_inputs() -> Dict[str, Union[int, float, bool]]: - """Parse inputs using AMReX ParmParse to_dict method.""" - pp = amr.ParmParse("") - - # Add inputs file if it exists - import os - inputs_file = "inputs" - if os.path.exists(inputs_file): - pp.addfile(inputs_file) - - # Default values with their types - defaults = { - 'n_cell': 32, - 'max_grid_size': 16, - 'nsteps': 1000, - 'plot_int': 100, - 'dt': 1.0e-5, - 'plot_files_output': False, - 'diffusion_coeff': 1.0, - 'init_amplitude': 1.0, - 'init_width': 0.01 - } - - try: - # Convert entire ParmParse table to Python dictionary - all_params = pp.to_dict() - - # Extract our specific parameters with proper type conversion - params = {} - for key, default_value in defaults.items(): - if key in all_params: - try: - # Convert string to appropriate type based on default - if isinstance(default_value, int): - params[key] = int(all_params[key]) - elif isinstance(default_value, float): - params[key] = float(all_params[key]) - elif isinstance(default_value, bool): - # Handle boolean conversion from string - val_str = str(all_params[key]).lower() - if val_str in ('true', '1', 'yes', 'on'): - params[key] = True - elif val_str in ('false', '0', 'no', 'off'): - params[key] = False - else: - # If unrecognized, use default - params[key] = default_value - else: - params[key] = all_params[key] - except (ValueError, TypeError) as e: - amr.Print(f"Warning: Could not convert parameter {key}='{all_params[key]}' to {type(default_value).__name__}, using default: {default_value}") - params[key] = default_value - else: - params[key] = default_value - - # Optional: print the parameters we're actually using - amr.Print("Using parameters:") - for key, value in params.items(): - amr.Print(f" {key}: {value}") - - return params - - except Exception as e: - amr.Print(f"Warning: Could not parse parameters with to_dict(): {e}") - amr.Print("Using default values") - return defaults - -if __name__ == '__main__': - # Initialize AMReX - amr.initialize([]) - - try: - # Parse inputs - params = parse_inputs() - - # Run simulation - main(**params) - - finally: - # Finalize AMReX - amr.finalize() diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/CMakeLists.txt b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/CMakeLists.txt deleted file mode 100644 index 1c005a0a..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/CMakeLists.txt +++ /dev/null @@ -1,77 +0,0 @@ -# CMake file for Heat Equation Python Driver Example -# -# This demonstrates the minimal setup needed for a Python interface -# to AMReX C++ simulation code. -# -# Step 1: Create and enter a build directory: -# -# mkdir build -# cd build -# -# Step 2: Configure with CMake from inside the build directory: -# -# cmake .. -# -# Step 3: Build the configuration: -# -# cmake --build . -j4 -# -# Step 4: Test the Python interface: -# -# python ../test.py - -cmake_minimum_required(VERSION 3.16) - -# Project name and source file languages -project(HeatEquation_PythonDriver - LANGUAGES C CXX) - -# Use pyamrex pybind11 infrastructure -include(pybind11.cmake) - -# Add the main executable -add_executable(HeatEquation_PythonDriver main.cpp) - -# Add the pybind11 module including main simulation logic -pybind11_add_module(amrex_heat bindings.cpp main.cpp) - -# To use a pre-installed AMReX build, run: -# cmake -DAMReX_ROOT=/path/to/installdir -# Otherwise cmake will download AMReX from GitHub - -if(NOT DEFINED AMReX_ROOT) - message("-- Download and configure AMReX from GitHub") - - #Download AMReX from GitHub - include(FetchContent) - set(FETCHCONTENT_QUIET OFF) - - # Force position-independent code for shared libraries - set(CMAKE_POSITION_INDEPENDENT_CODE ON) - - FetchContent_Declare( - amrex_code - GIT_REPOSITORY https://github.com/AMReX-Codes/amrex.git/ - GIT_TAG origin/development - ) - - FetchContent_Populate(amrex_code) - - # CMake will read the files in these directories to configure, build - # and install AMReX. - add_subdirectory(${amrex_code_SOURCE_DIR} ${amrex_code_BINARY_DIR}) - -else() - - # Add AMReX install - message("-- Searching for AMReX install directory at ${AMReX_ROOT}") - find_package(AMReX PATHS ${AMReX_ROOT}/lib/cmake/AMReX/AMReXConfig.cmake) - -endif() - -# Link AMReX to both targets -target_link_libraries(HeatEquation_PythonDriver PRIVATE AMReX::amrex) -target_link_libraries(amrex_heat PRIVATE AMReX::amrex) - -## Copy input files to build directory -file( COPY ${CMAKE_SOURCE_DIR}/inputs DESTINATION . ) \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/GNUmakefile b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/GNUmakefile deleted file mode 100644 index e4df1d85..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/GNUmakefile +++ /dev/null @@ -1,126 +0,0 @@ -# AMREX_HOME defines the directory in which we will find all the AMReX code. -AMREX_HOME ?= ../../../../amrex - -# For Python support, clone pybind11 as sibling to amrex: -# git clone -b v3.0.0 https://github.com/pybind/pybind11.git -# Or set PYBIND11_HOME to point to your pybind11 installation -PYBIND11_HOME ?= ../../../../pybind11 - -DEBUG = FALSE -USE_MPI = TRUE -USE_OMP = FALSE -COMP = gcc -DIM = 3 - -USE_CUDA = FALSE -USE_HIP = FALSE -USE_SYCL = FALSE - -# Python main control flag -ifndef USE_PYTHON_MAIN - USE_PYTHON_MAIN = FALSE -endif - -include $(AMREX_HOME)/Tools/GNUMake/Make.defs - -# Add pybind11 and Python includes when building for Python -ifeq ($(USE_PYTHON_MAIN),TRUE) - INCLUDE_LOCATIONS += $(PYBIND11_HOME)/include - - # Add Python headers - #PYTHON_CONFIG ?= python3-config - - # Add Python includes - #PYTHON_INCLUDES := $(shell $(PYTHON_CONFIG) --includes) - #CXXFLAGS += $(PYTHON_INCLUDES) - - # Add Python linking flags - try both methods - #PYTHON_LDFLAGS := $(shell $(PYTHON_CONFIG) --ldflags --embed 2>/dev/null || $(PYTHON_CONFIG) --ldflags) - #PYTHON_LIBS := $(shell $(PYTHON_CONFIG) --libs --embed 2>/dev/null || $(PYTHON_CONFIG) --libs) - - #LDFLAGS += $(PYTHON_LDFLAGS) $(PYTHON_LIBS) -endif - -# Add PIC flag when building for Python -ifeq ($(USE_PYTHON_MAIN),TRUE) - XTRA_CXXFLAGS += -fPIC -ifeq ($(USE_OMP),TRUE) - LDFLAGS += -fopenmp -endif -endif - -include Make.package -VPATH_LOCATIONS += . -INCLUDE_LOCATIONS += . - -include $(AMREX_HOME)/Src/Base/Make.package - -include $(AMREX_HOME)/Tools/GNUMake/Make.rules - -# Python library building (using pybind11 naming convention) -# Python library building (using pybind11 naming convention) -ifeq ($(USE_PYTHON_MAIN),TRUE) - -# Add pybind11 and Python configuration -INCLUDE_LOCATIONS += $(PYBIND11_HOME)/include -PYTHON_CONFIG ?= python3-config -CXXFLAGS += $(shell $(PYTHON_CONFIG) --includes) -CXXFLAGS += -fPIC - -# Python linking flags for the shared library -PYTHON_LDFLAGS := $(shell $(PYTHON_CONFIG) --ldflags --embed 2>/dev/null || $(PYTHON_CONFIG) --ldflags) -PYTHON_LIBS := $(shell $(PYTHON_CONFIG) --libs --embed 2>/dev/null || $(PYTHON_CONFIG) --libs) - -# In WarpX this is machine-dependent -SHARED_OPTION = -shared - -# Pybind11-style naming -PYMODULE_NAME = amrex_heat -PYDIM = $(DIM)d - -# Build both main executable and Python library -all: $(main_exe) lib$(PYMODULE_NAME).$(PYDIM).so - -installamrex_heat: lib$(PYMODULE_NAME).$(PYDIM).so - cp lib$(PYMODULE_NAME).$(PYDIM).so Python/py$(PYMODULE_NAME) - cd Python; python3 setup.py install --force --with-lib$(PYMODULE_NAME) $(PYDIM) $(PYINSTALLOPTIONS) - -lib$(PYMODULE_NAME).$(PYDIM).a: $(objForExecs) - @echo Making static library $@ ... - $(SILENT) $(AR) -crv $@ $^ - $(SILENT) $(RM) AMReX_buildInfo.cpp - @echo SUCCESS - -lib$(PYMODULE_NAME).$(PYDIM).so: $(objForExecs) - @echo Making dynamic library $@ ... -ifeq ($(USE_CUDA),TRUE) - $(SILENT) $(CXX) $(LINKFLAGS) $(SHARED_OPTION) -Xlinker=$(SHARED_OPTION) -Xlinker=-fPIC -o $@ $^ $(LDFLAGS) $(libraries) $(PYTHON_LDFLAGS) $(PYTHON_LIBS) -else - $(SILENT) $(CXX) $(LINKFLAGS) $(SHARED_OPTION) -fPIC -o $@ $^ $(LDFLAGS) $(libraries) $(PYTHON_LDFLAGS) $(PYTHON_LIBS) -endif - $(SILENT) $(RM) AMReX_buildInfo.cpp - @echo SUCCESS - -clean:: - $(SILENT) $(RM) -rf build - $(SILENT) $(RM) -f lib$(PYMODULE_NAME).?d.a - $(SILENT) $(RM) -f lib$(PYMODULE_NAME).?d.so - $(SILENT) $(RM) -f py$(PYMODULE_NAME)/lib$(PYMODULE_NAME).?d.so - $(SILENT) $(RM) -f $(PYMODULE_NAME).*.so # Clean pybind11-style names too - -else -# When USE_PYTHON_MAIN=FALSE, only build main executable -# (existing AMReX build system handles this) -endif - -# Convenience targets -.PHONY: python_lib python_install inputs - -python_lib: - $(MAKE) USE_PYTHON_MAIN=TRUE lib$(PYMODULE_NAME).$(DIM)d.so - -python_install: - $(MAKE) USE_PYTHON_MAIN=TRUE installamrex_heat - -inputs: - cp -r inputs/* . diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/Make.package b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/Make.package deleted file mode 100644 index 21ecf512..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/Make.package +++ /dev/null @@ -1,6 +0,0 @@ -CEXE_sources += main.cpp - -# Add bindings when building for Python -ifeq ($(USE_PYTHON_MAIN),TRUE) - CEXE_sources += bindings.cpp -endif \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/README.md b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/README.md deleted file mode 100644 index ed0a9f8a..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Heat Equation Python Driver - -This example demonstrates the minimal setup needed to create a one-line Python interface for AMReX C++ simulation codes. - -## Key Feature - -The main goal is to enable this simple Python usage pattern: - -```python -import amrex_heat - -# One-line simulation execution with structured results -result = amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"]) - -print(f"Max temperature: {result.max_temperature}") -print(f"Final time: {result.final_time}") -``` - -## Files - -- `main.cpp` - Heat equation solver with `heat_equation_main()` function -- `bindings.cpp` - Minimal pybind11 interface (just the one-liner) -- `CMakeLists.txt` - Build configuration with pybind11 support -- `pybind11.cmake` - Pybind11 infrastructure from pyamrex -- `inputs` - Simulation parameters -- `test.py` - Example Python usage - -## Architecture - -The key architectural insight is the function refactoring pattern: - -```cpp -// Reusable simulation function -SimulationResult heat_equation_main(int argc, char* argv[]) { - // All simulation logic here - return result; -} - -// C++ entry point -int main(int argc, char* argv[]) { - heat_equation_main(argc, argv); - return 0; -} -``` - -This allows the same simulation code to be called from: -- Command line: `./HeatEquation_PythonDriver inputs` -- Python: `amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"])` - -## Building - -```bash -mkdir build && cd build -cmake .. -make -j4 -``` - -## Running - -### C++ executable -```bash -cd build -./HeatEquation_PythonDriver inputs -``` - -### Python interface -```bash -cd build -python ../test.py -``` - -## What's Minimal - -This example includes only the essential components: - -1. **SimulationResult struct** - Simple data structure for return values -2. **heat_equation_main function** - Refactored simulation code -3. **Minimal bindings** - Just `run_simulation()` function -4. **Basic CMake** - Pybind11 integration - -No callback system, no complex data structures, no advanced features - just the core one-liner interface. - -## Extending - -This foundation can be extended with: -- Callback system for progress monitoring -- More return data (arrays, MultiFabs) -- Parameter setting from Python -- Integration with full pyamrex infrastructure \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/bindings.H b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/bindings.H deleted file mode 100644 index b547df36..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/bindings.H +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef SIMULATION_RESULT_H -#define SIMULATION_RESULT_H - -struct SimulationResult { - double max_temperature; - double std_temperature; // Add this field if you want it - int final_step; - double final_time; - bool success; -}; - -// Function declaration -SimulationResult heat_equation_main(int argc, char* argv[]); - -#endif // SIMULATION_RESULT_H diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/bindings.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/bindings.cpp deleted file mode 100644 index 727bbb30..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/bindings.cpp +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Generic Model Interface with automatic default parameter handling - */ - -#include -#include -#include -#include -#include -#include - -namespace py = pybind11; - -/* - * FORWARD DECLARATIONS - * These must match your actual function signatures in main.cpp - */ -#include "bindings.H" - -SimulationResult heat_equation_main(int argc, char* argv[]); - -/* - * STANDARD MODEL INTERFACE - */ -class ModelInterface { -public: - virtual std::vector get_pnames() const = 0; - virtual std::vector get_outnames() const = 0; - virtual std::vector> get_param_margpc() const = 0; - - // Run simulation - missing parameters automatically filled with defaults - virtual std::map run_single(const std::map& params = {}) = 0; -}; - -/* - * HEAT EQUATION MODEL IMPLEMENTATION - */ -class HeatEquationModel : public ModelInterface { -public: - std::vector get_pnames() const override { - return {"diffusion", "amplitude", "width"}; - } - - std::vector get_outnames() const override { - return {"max_temperature", "std_temperature"}; - } - - std::vector> get_param_margpc() const override { - return { - {1.0, 0.2}, // diffusion: [mean, std_dev] - {1.5, 0.3}, // amplitude: [mean, std_dev] - {0.01, 0.002} // width: [mean, std_dev] - }; - } - - std::map run_single(const std::map& params = {}) override { - auto pnames = get_pnames(); - auto margpc = get_param_margpc(); - - /* - * AUTOMATIC DEFAULT HANDLING - * Start with defaults (means from marginal PC distributions) - * Then override with any user-provided parameters - */ - std::map full_params; - - // Fill in defaults first (use PC means) - for (size_t i = 0; i < pnames.size(); ++i) { - full_params[pnames[i]] = margpc[i][0]; // margpc[i][0] = mean - } - - // Override with any provided parameters - for (const auto& [name, value] : params) { - if (full_params.find(name) != full_params.end()) { - full_params[name] = value; - } else { - throw std::runtime_error("Unknown parameter: " + name); - } - } - - /* - * BUILD COMMAND LINE ARGUMENTS - * Automatically construct args from parameter map - */ - std::vector args_str = {"./HeatEquation_PythonDriver", "inputs"}; - - for (const auto& [name, value] : full_params) { - args_str.push_back(name + "=" + std::to_string(value)); - } - - /* - * RUN SIMULATION - */ - std::vector args_cstr; - for (auto& s : args_str) { - args_cstr.push_back(const_cast(s.c_str())); - } - args_cstr.push_back(nullptr); - - auto result = heat_equation_main(static_cast(args_cstr.size() - 1), args_cstr.data()); - - if (!result.success) { - throw std::runtime_error("Simulation failed"); - } - - /* - * RETURN RESULTS - * Map output names to values - fixed initialization syntax - */ - std::map output_map; - output_map["max_temperature"] = result.max_temperature; - output_map["std_temperature"] = result.std_temperature; - - return output_map; - } -}; - -/* - * GENERIC BATCH RUNNER - */ -py::array_t run_batch_generic( - ModelInterface& model, - py::array_t params -) { - auto pnames = model.get_pnames(); - auto outnames = model.get_outnames(); - - auto buf = params.request(); - if (buf.ndim != 2 || buf.shape[1] != static_cast(pnames.size())) { - throw std::runtime_error("Parameters must be (N, " + std::to_string(pnames.size()) + ") array"); - } - - size_t n_sims = buf.shape[0]; - double *ptr = static_cast(buf.ptr); - - std::vector shape = {static_cast(n_sims), static_cast(outnames.size())}; - auto result = py::array_t(shape); - auto result_buf = result.request(); - double *result_ptr = static_cast(result_buf.ptr); - - for (size_t i = 0; i < n_sims; ++i) { - // Build parameter map - std::map param_map; - for (size_t j = 0; j < pnames.size(); ++j) { - param_map[pnames[j]] = ptr[i * pnames.size() + j]; - } - - // Run simulation (defaults automatically handled) - auto outputs = model.run_single(param_map); - - // Store results - for (size_t j = 0; j < outnames.size(); ++j) { - result_ptr[i * outnames.size() + j] = outputs[outnames[j]]; - } - } - - return result; -} - -PYBIND11_MODULE(amrex_heat, m) { - m.doc() = "Generic AMReX Model Interface with automatic defaults"; - - py::class_(m, "ModelInterface") - .def("get_pnames", &ModelInterface::get_pnames) - .def("get_outnames", &ModelInterface::get_outnames) - .def("get_param_margpc", &ModelInterface::get_param_margpc) - .def("run_single", &ModelInterface::run_single, - "Run single simulation with optional parameters (missing params use defaults)", - py::arg("params") = py::dict()); - - py::class_(m, "HeatEquationModel") - .def(py::init<>()); - - m.def("run_batch_generic", &run_batch_generic, - "Run batch simulations", - py::arg("model"), py::arg("params")); -} diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/inputs b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/inputs deleted file mode 100644 index 5b098f82..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/inputs +++ /dev/null @@ -1,7 +0,0 @@ -n_cell = 32 -max_grid_size = 16 - -nsteps = 1000 -plot_int = 100 - -dt = 1.e-5 diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/main.cpp deleted file mode 100644 index 39a299d2..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/main.cpp +++ /dev/null @@ -1,288 +0,0 @@ -/* - * Simplified Heat Equation with Python Driver Interface - * - * This demonstrates the minimal setup needed for a one-line Python interface - * that calls C++ simulation code and returns structured results. - */ - - -#include -#include -#include - -#include "bindings.H" - -SimulationResult heat_equation_main(int argc, char* argv[]) -{ - SimulationResult result = {0.0, 0.0, 0, 0.0, false}; - - amrex::Initialize(argc,argv); - { - - // ********************************** - // DECLARE SIMULATION PARAMETERS - // ********************************** - - // number of cells on each side of the domain - int n_cell; - - // size of each box (or grid) - int max_grid_size; - - // total steps in simulation - int nsteps; - - // how often to write a plotfile - int plot_int; - - // time step - amrex::Real dt; - - // ********************************** - // DECLARE PHYSICS PARAMETERS - // ********************************** - - // diffusion coefficient for heat equation - amrex::Real diffusion_coeff; - - // amplitude of initial temperature profile - amrex::Real init_amplitude; - - // width parameter controlling spread of initial profile (variance, not std dev) - amrex::Real init_width; - - // ********************************** - // READ PARAMETER VALUES FROM INPUT DATA - // ********************************** - // inputs parameters - { - // ParmParse is way of reading inputs from the inputs file - // pp.get means we require the inputs file to have it - // pp.query means we optionally need the inputs file to have it - but we must supply a default here - amrex::ParmParse pp; - - // We need to get n_cell from the inputs file - this is the number of cells on each side of - // a square (or cubic) domain. - pp.get("n_cell",n_cell); - - // The domain is broken into boxes of size max_grid_size - pp.get("max_grid_size",max_grid_size); - - // Default nsteps to 10, allow us to set it to something else in the inputs file - nsteps = 10; - pp.query("nsteps",nsteps); - - // Default plot_int to -1, allow us to set it to something else in the inputs file - // If plot_int < 0 then no plot files will be written - plot_int = -1; - pp.query("plot_int",plot_int); - - // time step - pp.get("dt",dt); - - // ********************************** - // READ PHYSICS PARAMETERS - // ********************************** - - // Diffusion coefficient - controls how fast heat spreads - diffusion_coeff = 1.0; - pp.query("diffusion", diffusion_coeff); // Note: input name is "diffusion" but variable is "diffusion_coeff" - - // Initial temperature amplitude - init_amplitude = 1.0; - pp.query("amplitude", init_amplitude); - - // Width parameter - this is the variance (width²), not standard deviation - // Smaller values = more concentrated, larger values = more spread out - init_width = 0.01; // Note: 0.01 to match your original rsquared/0.01 - pp.query("width", init_width); - } - - // ********************************** - // DEFINE SIMULATION SETUP AND GEOMETRY - // ********************************** - - // make BoxArray and Geometry - // ba will contain a list of boxes that cover the domain - // geom contains information such as the physical domain size, - // number of points in the domain, and periodicity - amrex::BoxArray ba; - amrex::Geometry geom; - - // define lower and upper indices - amrex::IntVect dom_lo(0,0,0); - amrex::IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); - - // Make a single box that is the entire domain - amrex::Box domain(dom_lo, dom_hi); - - // Initialize the boxarray "ba" from the single box "domain" - ba.define(domain); - - // Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction - ba.maxSize(max_grid_size); - - // This defines the physical box, [0,1] in each direction. - amrex::RealBox real_box({ 0., 0., 0.}, - { 1., 1., 1.}); - - // periodic in all direction - amrex::Array is_periodic{1,1,1}; - - // This defines a Geometry object - geom.define(domain, real_box, amrex::CoordSys::cartesian, is_periodic); - - // extract dx from the geometry object - amrex::GpuArray dx = geom.CellSizeArray(); - - // Nghost = number of ghost cells for each array - int Nghost = 1; - - // Ncomp = number of components for each array - int Ncomp = 1; - - // How Boxes are distrubuted among MPI processes - amrex::DistributionMapping dm(ba); - - // we allocate two phi multifabs; one will store the old state, the other the new. - amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); - amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); - - // time = starting time in the simulation - amrex::Real time = 0.0; - - // ********************************** - // INITIALIZE DATA LOOP - // ********************************** - - // loop over boxes - for (amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi) - { - const amrex::Box& bx = mfi.validbox(); - - const amrex::Array4& phiOld = phi_old.array(mfi); - - // ********************************** - // SET INITIAL TEMPERATURE PROFILE - // ********************************** - // Formula: T = 1 + amplitude * exp(-r^2 / width^2) - // where r is distance from center (0.5, 0.5, 0.5) - // - // Parameters: - // - amplitude: controls peak temperature above baseline (1.0) - // - width: controls spread of initial hot spot - // - smaller width = more concentrated - // - larger width = more spread out - -amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) - { - // Calculate physical coordinates of cell center - amrex::Real x = (i+0.5) * dx[0]; - amrex::Real y = (j+0.5) * dx[1]; - amrex::Real z = (k+0.5) * dx[2]; - - // Calculate squared distance from domain center (0.5, 0.5, 0.5) - // Divide by init_width (which is the variance, not standard deviation) - amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; - - // Set initial temperature profile - phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); - }); - } - - // ********************************** - // WRITE INITIAL PLOT FILE - // ********************************** - - // Write a plotfile of the initial data if plot_int > 0 - if (plot_int > 0) - { - int step = 0; - const std::string& pltfile = amrex::Concatenate("plt",step,5); - WriteSingleLevelPlotfile(pltfile, phi_old, {"phi"}, geom, time, 0); - } - - - // ********************************** - // MAIN TIME EVOLUTION LOOP - // ********************************** - - for (int step = 1; step <= nsteps; ++step) - { - // fill periodic ghost cells - phi_old.FillBoundary(geom.periodicity()); - - // new_phi = old_phi + dt * Laplacian(old_phi) - // loop over boxes - for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ) - { - const amrex::Box& bx = mfi.validbox(); - - const amrex::Array4& phiOld = phi_old.array(mfi); - const amrex::Array4& phiNew = phi_new.array(mfi); - - // advance the data by dt using heat equation with diffusion coefficient - amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) - { - // Calculate the discrete Laplacian using finite differences - amrex::Real laplacian = - (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) - +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) - +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]); - - // Apply heat equation using diffusion_coeff - matches Python version - phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; - }); - } - - // ********************************** - // INCREMENT - // ********************************** - - // update time - time = time + dt; - - // copy new solution into old solution - amrex::MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); - - // Update result with current state - result.final_step = step; - result.final_time = time; - - // Tell the I/O Processor to write out which step we're doing - amrex::Print() << "Advanced step " << step << "\n"; - - - // ********************************** - // WRITE PLOTFILE AT GIVEN INTERVAL - // ********************************** - - // Write a plotfile of the current data (plot_int was defined in the inputs file) - if (plot_int > 0 && step%plot_int == 0) - { - const std::string& pltfile = amrex::Concatenate("plt",step,5); - WriteSingleLevelPlotfile(pltfile, phi_new, {"phi"}, geom, time, step); - } - } - - // Get final max temperature and mark success - result.max_temperature = phi_new.max(0); - - // Calculate standard deviation of temperature field - amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); - result.std_temperature = std::sqrt(phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp); - - result.success = true; - - } - amrex::Finalize(); - return result; -} - -int main(int argc, char* argv[]) -{ - heat_equation_main(argc, argv); - return 0; -} - - diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/pybind11.cmake b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/pybind11.cmake deleted file mode 100644 index 45ba7898..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/pybind11.cmake +++ /dev/null @@ -1,63 +0,0 @@ -function(find_pybind11) - if(TARGET pybind11::module) - message(STATUS "pybind11::module target already imported") - elseif(pyAMReX_pybind11_src) - message(STATUS "Compiling local pybind11 ...") - message(STATUS "pybind11 source path: ${pyAMReX_pybind11_src}") - if(NOT IS_DIRECTORY ${pyAMReX_pybind11_src}) - message(FATAL_ERROR "Specified directory pyAMReX_pybind11_src='${pyAMReX_pybind11_src}' does not exist!") - endif() - elseif(pyAMReX_pybind11_internal) - message(STATUS "Downloading pybind11 ...") - message(STATUS "pybind11 repository: ${pyAMReX_pybind11_repo} (${pyAMReX_pybind11_branch})") - include(FetchContent) - endif() - - # rely on our find_package(Python ...) call - # https://pybind11.readthedocs.io/en/stable/compiling.html#modules-with-cmake - set(PYBIND11_FINDPYTHON ON) - - if(TARGET pybind11::module) - # nothing to do, target already exists in the superbuild - elseif(pyAMReX_pybind11_internal OR pyAMReX_pybind11_src) - set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) - - if(pyAMReX_pybind11_src) - add_subdirectory(${pyAMReX_pybind11_src} _deps/localpybind11-build/) - else() - FetchContent_Declare(fetchedpybind11 - GIT_REPOSITORY ${pyAMReX_pybind11_repo} - GIT_TAG ${pyAMReX_pybind11_branch} - BUILD_IN_SOURCE 0 - ) - FetchContent_MakeAvailable(fetchedpybind11) - - # advanced fetch options - mark_as_advanced(FETCHCONTENT_BASE_DIR) - mark_as_advanced(FETCHCONTENT_FULLY_DISCONNECTED) - mark_as_advanced(FETCHCONTENT_QUIET) - mark_as_advanced(FETCHCONTENT_SOURCE_DIR_FETCHEDpybind11) - mark_as_advanced(FETCHCONTENT_UPDATES_DISCONNECTED) - mark_as_advanced(FETCHCONTENT_UPDATES_DISCONNECTED_FETCHEDpybind11) - endif() - elseif(NOT pyAMReX_pybind11_internal) - find_package(pybind11 3.0.0 CONFIG REQUIRED) - message(STATUS "pybind11: Found version '${pybind11_VERSION}'") - endif() -endfunction() - -# local source-tree -set(pyAMReX_pybind11_src "" - CACHE PATH - "Local path to pybind11 source directory (preferred if set)") - -# Git fetcher -option(pyAMReX_pybind11_internal "Download & build pybind11" ON) -set(pyAMReX_pybind11_repo "https://github.com/pybind/pybind11.git" - CACHE STRING - "Repository URI to pull and build pybind11 from if(pyAMReX_pybind11_internal)") -set(pyAMReX_pybind11_branch "v3.0.0" - CACHE STRING - "Repository branch for pyAMReX_pybind11_repo if(pyAMReX_pybind11_internal)") - -find_pybind11() diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/test.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-3/test.py deleted file mode 100644 index 6aacaeef..00000000 --- a/GuidedTutorials/HeatEquation_PythonDriver/Case-3/test.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple test of the Heat Equation Python Driver - -This demonstrates the minimal one-line interface to run AMReX simulations -from Python and get structured results back. -""" - -import amrex_heat - -def main(): - print("Heat Equation Python Driver Test") - print("=" * 40) - - # The key feature: one-line simulation execution - print("Running simulation...") - result = amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"]) - - # Access results directly - print(f"\nSimulation Results:") - print(f" Success: {result.success}") - print(f" Final step: {result.final_step}") - print(f" Final time: {result.final_time:.6f}") - print(f" Max temperature: {result.max_temperature:.6f}") - - # You can use the results for further processing - if result.success: - print(f"\n✓ Simulation completed successfully!") - print(f" Temperature decay: {2.0 - result.max_temperature:.6f}") - else: - print(f"\n✗ Simulation failed!") - return 1 - - return 0 - -if __name__ == "__main__": - exit(main()) \ No newline at end of file From ffb90c48e7ac1d77845f6897c297e53ceea68a9e Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 6 Oct 2025 07:11:53 -0700 Subject: [PATCH 072/142] Moved files back to seperate directories --- .../HeatEquation_PythonDriver/{Case-0 => Case-1}/main.cpp | 0 .../{ => Case-2}/Case-0/AMReXBaseModel.py | 0 .../HeatEquation_PythonDriver/{ => Case-2}/Case-0/GNUmakefile | 0 .../{ => Case-2}/Case-0/HeatEquationModel.py | 0 .../HeatEquation_PythonDriver/{ => Case-2}/Case-0/Make.package | 0 .../HeatEquation_PythonDriver/{ => Case-2}/Case-0/inputs | 0 .../HeatEquation_PythonDriver/{ => Case-2}/Case-0/main.py | 0 .../{ => Case-2}/Case-0/model_wrapper.sh | 0 .../{ => Case-2}/Case-0/postprocess_datalog.sh | 0 9 files changed, 0 insertions(+), 0 deletions(-) rename GuidedTutorials/HeatEquation_PythonDriver/{Case-0 => Case-1}/main.cpp (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-2}/Case-0/AMReXBaseModel.py (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-2}/Case-0/GNUmakefile (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-2}/Case-0/HeatEquationModel.py (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-2}/Case-0/Make.package (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-2}/Case-0/inputs (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-2}/Case-0/main.py (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-2}/Case-0/model_wrapper.sh (100%) rename GuidedTutorials/HeatEquation_PythonDriver/{ => Case-2}/Case-0/postprocess_datalog.sh (100%) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp b/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.cpp rename to GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/AMReXBaseModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-0/AMReXBaseModel.py rename to GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/AMReXBaseModel.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/GNUmakefile b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/GNUmakefile similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-0/GNUmakefile rename to GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/GNUmakefile diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/HeatEquationModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-0/HeatEquationModel.py rename to GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/HeatEquationModel.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/Make.package b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/Make.package similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-0/Make.package rename to GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/Make.package diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/inputs b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/inputs similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-0/inputs rename to GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/inputs diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.py b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/main.py similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-0/main.py rename to GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/main.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/model_wrapper.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/model_wrapper.sh similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-0/model_wrapper.sh rename to GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/model_wrapper.sh diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-0/postprocess_datalog.sh b/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/postprocess_datalog.sh similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-0/postprocess_datalog.sh rename to GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/postprocess_datalog.sh From b7f6500f74d41bbb2ddbfdda10780cf5dee9ef3f Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 6 Oct 2025 07:12:49 -0700 Subject: [PATCH 073/142] Rename overall directory --- .../Case-1/CMakeLists.txt | 0 .../Case-1/GNUmakefile | 0 .../Case-1/Make.package | 0 .../{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-1/inputs | 0 .../Case-1/main.cpp | 0 .../Case-1/pnames.txt | 0 .../Case-2/Case-0/AMReXBaseModel.py | 0 .../Case-2/Case-0/GNUmakefile | 0 .../Case-2/Case-0/HeatEquationModel.py | 0 .../Case-2/Case-0/Make.package | 0 .../Case-2/Case-0/inputs | 0 .../Case-2/Case-0/main.py | 0 .../Case-2/Case-0/model_wrapper.sh | 0 .../Case-2/Case-0/postprocess_datalog.sh | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-1/CMakeLists.txt (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-1/GNUmakefile (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-1/Make.package (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-1/inputs (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-1/main.cpp (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-1/pnames.txt (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-2/Case-0/AMReXBaseModel.py (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-2/Case-0/GNUmakefile (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-2/Case-0/HeatEquationModel.py (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-2/Case-0/Make.package (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-2/Case-0/inputs (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-2/Case-0/main.py (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-2/Case-0/model_wrapper.sh (100%) rename GuidedTutorials/{HeatEquation_PythonDriver => HeatEquation_UQ}/Case-2/Case-0/postprocess_datalog.sh (100%) diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/CMakeLists.txt b/GuidedTutorials/HeatEquation_UQ/Case-1/CMakeLists.txt similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/CMakeLists.txt rename to GuidedTutorials/HeatEquation_UQ/Case-1/CMakeLists.txt diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile b/GuidedTutorials/HeatEquation_UQ/Case-1/GNUmakefile similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/GNUmakefile rename to GuidedTutorials/HeatEquation_UQ/Case-1/GNUmakefile diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package b/GuidedTutorials/HeatEquation_UQ/Case-1/Make.package similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/Make.package rename to GuidedTutorials/HeatEquation_UQ/Case-1/Make.package diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs b/GuidedTutorials/HeatEquation_UQ/Case-1/inputs similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/inputs rename to GuidedTutorials/HeatEquation_UQ/Case-1/inputs diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/main.cpp rename to GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-1/pnames.txt b/GuidedTutorials/HeatEquation_UQ/Case-1/pnames.txt similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-1/pnames.txt rename to GuidedTutorials/HeatEquation_UQ/Case-1/pnames.txt diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/AMReXBaseModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/AMReXBaseModel.py rename to GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/AMReXBaseModel.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/GNUmakefile b/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/GNUmakefile similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/GNUmakefile rename to GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/GNUmakefile diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/HeatEquationModel.py b/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/HeatEquationModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/HeatEquationModel.py rename to GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/HeatEquationModel.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/Make.package b/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/Make.package similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/Make.package rename to GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/Make.package diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/inputs b/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/inputs similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/inputs rename to GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/inputs diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/main.py b/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/main.py similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/main.py rename to GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/main.py diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/model_wrapper.sh b/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/model_wrapper.sh similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/model_wrapper.sh rename to GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/model_wrapper.sh diff --git a/GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/postprocess_datalog.sh b/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/postprocess_datalog.sh similarity index 100% rename from GuidedTutorials/HeatEquation_PythonDriver/Case-2/Case-0/postprocess_datalog.sh rename to GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/postprocess_datalog.sh From 04a0033a3494853399c357a13e61b69ceb701f4d Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 6 Oct 2025 07:15:57 -0700 Subject: [PATCH 074/142] Fix level --- .../HeatEquation_UQ/Case-2/{Case-0 => }/AMReXBaseModel.py | 0 GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/GNUmakefile | 0 .../HeatEquation_UQ/Case-2/{Case-0 => }/HeatEquationModel.py | 0 GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/Make.package | 0 GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/inputs | 0 GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/main.py | 0 .../HeatEquation_UQ/Case-2/{Case-0 => }/model_wrapper.sh | 0 .../HeatEquation_UQ/Case-2/{Case-0 => }/postprocess_datalog.sh | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/AMReXBaseModel.py (100%) rename GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/GNUmakefile (100%) rename GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/HeatEquationModel.py (100%) rename GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/Make.package (100%) rename GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/inputs (100%) rename GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/main.py (100%) rename GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/model_wrapper.sh (100%) rename GuidedTutorials/HeatEquation_UQ/Case-2/{Case-0 => }/postprocess_datalog.sh (100%) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/AMReXBaseModel.py rename to GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/GNUmakefile b/GuidedTutorials/HeatEquation_UQ/Case-2/GNUmakefile similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/GNUmakefile rename to GuidedTutorials/HeatEquation_UQ/Case-2/GNUmakefile diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/HeatEquationModel.py b/GuidedTutorials/HeatEquation_UQ/Case-2/HeatEquationModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/HeatEquationModel.py rename to GuidedTutorials/HeatEquation_UQ/Case-2/HeatEquationModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/Make.package b/GuidedTutorials/HeatEquation_UQ/Case-2/Make.package similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/Make.package rename to GuidedTutorials/HeatEquation_UQ/Case-2/Make.package diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/inputs b/GuidedTutorials/HeatEquation_UQ/Case-2/inputs similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/inputs rename to GuidedTutorials/HeatEquation_UQ/Case-2/inputs diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/main.py b/GuidedTutorials/HeatEquation_UQ/Case-2/main.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/main.py rename to GuidedTutorials/HeatEquation_UQ/Case-2/main.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/model_wrapper.sh b/GuidedTutorials/HeatEquation_UQ/Case-2/model_wrapper.sh similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/model_wrapper.sh rename to GuidedTutorials/HeatEquation_UQ/Case-2/model_wrapper.sh diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/postprocess_datalog.sh b/GuidedTutorials/HeatEquation_UQ/Case-2/postprocess_datalog.sh similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/Case-0/postprocess_datalog.sh rename to GuidedTutorials/HeatEquation_UQ/Case-2/postprocess_datalog.sh From f3ec4bac3dc3d7a89e81ccb5b3cc0d8a66917eef Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 6 Oct 2025 07:16:47 -0700 Subject: [PATCH 075/142] Rename so datalog modelx only one --- .../HeatEquation_UQ/Case-2/{model_wrapper.sh => model.x} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename GuidedTutorials/HeatEquation_UQ/Case-2/{model_wrapper.sh => model.x} (100%) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/model_wrapper.sh b/GuidedTutorials/HeatEquation_UQ/Case-2/model.x similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/model_wrapper.sh rename to GuidedTutorials/HeatEquation_UQ/Case-2/model.x From e4110acd637953838aebdecceea13938bdc3e3d1 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 6 Oct 2025 07:22:20 -0700 Subject: [PATCH 076/142] Remove extra docs --- Docs/source/HeatEquation_UQ.rst | 295 +------------------------------- 1 file changed, 6 insertions(+), 289 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index ae7a4770..13cf7b05 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -123,56 +123,6 @@ Install the following components in order: - PyTUQ: v1.0.0z - amrex-tutorials: development branch (or jmsexton03/add_pybind_interface_test for testing) -PyTUQ Model Interface Examples -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -PyTUQ supports various model types, all following the same interface pattern: - -.. code-block:: python - :caption: Example 1: Linear model from PyTUQ tests - - def linear_model(x, par): - y = par['b'] + x @ par['W'] - return y - - # Usage with PyTUQ - W = np.random.randn(pdim, npt) - b = np.random.randn(npt) - true_model_params = {'W': W, 'b': b} - - # Model wrapper for PyTUQ - model = lambda x: linear_model(x, true_model_params) - outputs = model(inputs) # inputs: [n_samples, pdim] - -.. code-block:: python - :caption: Example 2: Simple function model - - # Direct lambda function - model = lambda x: x[:,0]**4 - 2.*x[:,0]**3 - - # Or as a class method - class Ishigami: - def __call__(self, x): - # Ishigami function implementation - return y - - model = Ishigami() - outputs = model(inputs) - -.. code-block:: python - :caption: Example 3: External executable wrapper - - def model(x): - # Write inputs to file - np.savetxt('inputs.txt', x) - - # Run external simulation - os.system('./model.x inputs.txt outputs.txt') - - # Load and return outputs - y = np.loadtxt('outputs.txt').reshape(x.shape[0], -1) - return y - Reference PyTUQ Workflow Examples ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -197,83 +147,6 @@ PyTUQ provides several workflow examples demonstrating the model interface: - `ex_lreg_merr.py `_ - ``y = X @ beta + error`` -Input/Output Specifications -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -PyTUQ expects data as NumPy arrays with specific shapes: - -.. code-block:: python - - # Standard interface for all models - def model(inputs): - """ - Args: - inputs: np.ndarray of shape [n_samples, n_parameters] - - Returns: - outputs: np.ndarray of shape [n_samples, n_outputs] - """ - return outputs - -**Heat Equation Input Parameters** (from AMReX inputs file): - -.. code-block:: text - :caption: Example inputs file with UQ parameters - - # Grid/Domain parameters - n_cell = 32 # number of cells on each side of domain - max_grid_size = 16 # size of each box (or grid) - - # Time stepping parameters - nsteps = 100 # total steps in simulation - dt = 1.e-5 # time step - - # Output control - plot_int = -1 # how often to write plotfile (-1 = no plots) - datalog_int = -1 # how often to write datalog (-1 = no regular output) - - # Physics parameters (these are what we vary for UQ) - diffusion_coeff = 1.0 # diffusion coefficient for heat equation - init_amplitude = 1.0 # amplitude of initial temperature profile - init_width = 0.01 # width parameter (variance, not std dev) - -**Heat Equation Output Format** (datalog): - -.. code-block:: text - :caption: Datalog output format - - # time max_temp std_temp final_step - 0.01 1.09628 0 1000 - -For UQ analysis, we typically vary the physics parameters (``diffusion_coeff``, ``init_amplitude``, ``init_width``) -and extract the outputs (``max_temp``, ``std_temp``) at the final timestep. - -.. warning:: - - Ensure your outputs are well-correlated with the input parameters being varied. - Outputs unaffected by input changes will produce meaningless UQ results. - -Choosing Your Integration Approach ------------------------------------ - -All approaches ultimately provide the same ``outputs = model(inputs)`` interface: - -.. code-block:: text - - Start: Need outputs = model(inputs) interface - │ - ├─ Using Python already? - │ ├─ Yes → Have pyAMReX? - │ │ ├─ Yes → Case 2: Direct Python model - │ │ └─ Using PICMI/WarpX? → Case 4: PICMI wrapper - │ └─ No → Continue to C++ options - │ - └─ C++ Application (need wrapper) - ├─ Centralized outputs and recompling? → Case 1a: Datalog wrapper - ├─ Bash configuration with no C++ code changes? → Case 1b: Bash wrapper - ├─ Fextract or fcompare workflows? → Case 1c: Fextract wrapper - └─ Want Python bindings? → Case 3: Pybind11 wrapper - Integration Cases ----------------- @@ -332,13 +205,13 @@ All C++ HeatEquation cases (1a, 1b, 1c) require the same parameter modifications The cases differ only in output extraction method: -Case 1a: C++ with Datalog Output +Case 1: C++ with Datalog Output ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. admonition:: When to use :class: tip - Choose when you have centralized output locations and want efficient I/O. + Choose when you have centralized output locations and want simple I/O. **Implementation Steps:** @@ -390,7 +263,7 @@ Case 1a: C++ with Datalog Output amr.DataLog(0) << time << " " << max_temperature << " " << mean_temp << std::endl; } -3. Configure bash wrapper (``model_wrapper.x``): +3. Configure bash wrapper (``model.x``): .. code-block:: bash :caption: Configuration section of model_wrapper.x @@ -401,7 +274,7 @@ Case 1a: C++ with Datalog Output INCLUDE_HEADER=true POSTPROCESSOR="./postprocess_datalog.sh" - The ``model_wrapper.x`` script (based on PyTUQ workflow examples) handles: + The ``model.x`` script (based on PyTUQ workflow examples) handles: - Reading input parameters from file - Setting up AMReX inputs command line options with parameters @@ -425,106 +298,6 @@ Case 1a: C++ with Datalog Output PC_TYPE=HG # Hermite-Gaussian PC INPC_ORDER=1 -Case 1b: C++ with Plotfile/Bash Extraction -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. admonition:: When to use - :class: tip - - Choose when working with existing AMReX plotfile infrastructure. - -**Implementation Steps:** - -1. Use parametrized C++ code with ``plot_int > 0`` - -2. Set up files like pnames.txt - diffusion_coeff - init_amplitude - init_width - -3. Use the pytuq infrastructure for setting up the inputs, e.g. - .. code-block:: bash - :caption: Configuration for input PC coefficients - - ## (a) Given mean and standard deviation of each normal random parameter - echo "1 0.1 " > param_margpc.txt - echo "1 0.1" >> param_margpc.txt - echo ".01 0.0025" >> param_margpc.txt - PC_TYPE=HG # Hermite-Gaussian PC - INPC_ORDER=1 - -4. Configure bash wrapper (``model.x``): - - For plotfile header extraction (current implementation): - - .. code-block:: bash - :caption: Configuration section of model.x for plotfile extraction - - # Source external functions library - SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - source "$SCRIPT_DIR/functions.sh" - - # Configuration - EXE="main3d.gnu.ex" # AMReX executable - INPUTS="inputs" # AMReX inputs file - LEVEL=0 # AMReX refinement level to extract from - TARGET_I=16 # Target i coordinate (for 32^3 grid center) - TARGET_J=16 # Target j coordinate - TARGET_K=16 # Target k coordinate - INCLUDE_HEADER=true # Include column headers in output - - The ``model.x`` script with ``functions.sh`` implements plotfile extraction: - - **Workflow:** - - 1. **Run simulation**: Creates run directories and executes AMReX code - - 2. **Locate plotfile**: Finds the last generated plotfile (``plt*``) directory - - 3. **Parse plotfile structure**: - - - Reads ``Header`` file for variable names and component counts - - Reads ``Level_X/Cell_H`` for box definitions and min/max data - - Identifies which box contains target (i,j,k) coordinates - - 4. **Extract min/max values**: - - - For each variable component, extracts minimum and maximum values - - Values are taken from the box containing the target coordinates - - Creates output columns named ``__min/max`` - - **Key functions in functions.sh:** - - - ``process_single_plotfile()``: Main orchestrator for plotfile extraction - - ``get_last_plotfile()``: Finds most recent plotfile in run directory - - ``generate_outnames_from_combined_sources()``: Creates column names from Header/Cell_H - - ``find_box_index()``: Determines which AMReX box contains target coordinates - - ``extract_minmax_from_cell_header()``: Extracts actual min/max values - - **Example plotfile structure:** - - .. code-block:: text - - plt00010/ - ├── Header # Variable names, components, metadata - └── Level_0/ - ├── Cell_H # Box definitions and min/max data - └── Cell_D_00000 # Actual cell data (not used here) - - **Output format:** - - The extraction produces one line per simulation with min/max values for each - variable component at the target location: - - .. code-block:: text - - # phi_0_min phi_0_max phi_1_min phi_1_max ... - 1.234e-05 2.345e-04 3.456e-05 4.567e-04 ... - - This approach extracts summary statistics without reading the full cell data, - making it efficient for uncertainty quantification workflows where only - specific metrics are needed. - Case 2: PyAMReX Direct Integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -549,61 +322,6 @@ Case 2: PyAMReX Direct Integration model = HeatEquationModel() # model now provides the required interface -Case 3: C++ with Pybind11 Bindings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. admonition:: When to use - :class: tip - - When you want compiled performance with Python interface. - -Case 4: PICMI/WarpX Integration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. admonition:: When to use - :class: tip - - When you want full functionality with a Python main. - - [This example is a simplifiction and should be replaced with a link to picmi inputs] -.. code-block:: python - :caption: warpx_model.py - - from pywarpx import picmi - import numpy as np - - def warpx_model(inputs): - """ - WarpX model with PyTUQ interface - - Args: - inputs: [n_samples, n_params] - params = [E_max, plasma_density, ...] - """ - outputs = np.zeros((inputs.shape[0], 3)) - - for i, params in enumerate(inputs): - # Create simulation with parameters - sim = picmi.Simulation( - E_max=params[0], - n_plasma=params[1], - # ... other parameters - ) - - # Run simulation - sim.step(max_steps=1000) - - # Extract outputs - outputs[i, 0] = sim.get_field_energy()[-1] - outputs[i, 1] = sim.get_particle_energy()[-1] - outputs[i, 2] = sim.get_momentum_spread()[-1] - - return outputs - - # Ready for PyTUQ - model = warpx_model - - Running Complete UQ Workflows ------------------------------ @@ -645,6 +363,5 @@ Additional Resources For complete working examples of the ``outputs = model(inputs)`` pattern, see: - - ``amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/Case-1/`` - C++ wrappers - - ``amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/Case-2/`` - PyAMReX native - - ``amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/Case-3/`` - Pybind11 wrapper + - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-1/`` - C++ wrappers + - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-2/`` - PyAMReX native From 977cc17c5c1e108ae33bf6835fdf0933346e74da Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 6 Oct 2025 07:27:51 -0700 Subject: [PATCH 077/142] More simplifications --- Docs/source/HeatEquation_UQ.rst | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 13cf7b05..ee92ee81 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -153,13 +153,6 @@ Integration Cases Case 1: C++ Application Wrappers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -All three approaches create a Python wrapper providing the ``model(inputs)`` interface. - -Common C++ Modifications for HeatEquation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -All C++ HeatEquation cases (1a, 1b, 1c) require the same parameter modifications: - **Implementation Steps:** 1. Add physics parameters to main.cpp declarations: @@ -199,11 +192,10 @@ All C++ HeatEquation cases (1a, 1b, 1c) require the same parameter modifications The key to PyTUQ integration is creating a loop that maps input parameters to output quantities. This loop structure differs between cases: - - **Case 1**: External Python loop writes inputs files, runs executable, parses outputs - - **Case 2**: Python loop directly calls pyAMReX functions - - **Case 3**: Python loop calls pybind11-wrapped C++ functions + - **Case 1**: Bash workflow manages run directories, runs executable with input parameters, parses outputs + - **Case 2**: Python loop directly calls pyAMReX functions -The cases differ only in output extraction method: +The simplest output extraction method is described here: Case 1: C++ with Datalog Output ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 216a825c29cdeb7ce1fdd1dcced2bdbda0f7ccdf Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 6 Oct 2025 14:44:53 -0700 Subject: [PATCH 078/142] Summarize --- Docs/source/HeatEquation_UQ.rst | 90 +++++-------------- .../example_detailed_install.sh | 60 +++++++++++++ 2 files changed, 81 insertions(+), 69 deletions(-) create mode 100644 GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index ee92ee81..7c12f908 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -19,19 +19,12 @@ PyTUQ Integration with AMReX Applications Goals ----- -This tutorial demonstrates how to integrate PyTUQ (Python Uncertainty Quantification Toolkit) with AMReX-based applications. PyTUQ expects models to follow a simple interface: - -.. code-block:: python - - # Generic PyTUQ model interface - outputs = model(inputs) # Both are numpy arrays - # inputs shape: [n_samples, n_parameters] - # outputs shape: [n_samples, n_outputs] +This tutorial demonstrates how to integrate PyTUQ (Python Uncertainty Quantification Toolkit) with AMReX-based applications. You will learn to: -1. Wrap AMReX simulations to provide this interface -2. Extract and format simulation outputs as numpy arrays +1. Configure wrappers for AMReX simulations to interface to PyTUQ +2. Use inputs and extract datalog outputs 3. Choose the appropriate integration approach for your workflow 4. Run sensitivity analysis and inverse modeling using PyTUQ @@ -41,10 +34,10 @@ Prerequisites and Setup Required Dependencies ~~~~~~~~~~~~~~~~~~~~~ -Install the following components in order: +Install pytuq as described in `pytuq/README.md `_: .. code-block:: bash - :caption: Complete installation script + :caption: Pytuq installation script #!/bin/bash @@ -52,61 +45,29 @@ Install the following components in order: # 1. Clone repositories git clone --recursive --branch v1.0.0z https://github.com/sandialabs/pytuq - git clone --branch 25.10 https://github.com/amrex-codes/pyamrex - git clone --branch 25.10 https://github.com/amrex-codes/amrex - git clone --branch development https://github.com/amrex-codes/amrex-tutorials - # Alternative: git clone --branch add_pybind_interface_test https://github.com/jmsexton03/amrex-tutorials - # 2. Setup conda environment + # 2. Setup conda environment (optional, you can add to an existing env) # Create conda environment (use -y for non-interactive) - conda create -y --name pyamrex_pytuq python=3.11 --no-default-packages + conda create -y --name pytuq_integration python=3.11 --no-default-packages # For NERSC (see https://docs.nersc.gov/development/languages/python/nersc-python/#moving-your-conda-setup-to-globalcommonsoftware): - # conda create -y --prefix /global/common/software/myproject/$USER/pyamrex_pytuq python=3.11 - - conda activate pyamrex_pytuq - # For NERSC: conda activate /global/common/software/myproject/$USER/pyamrex_pytuq - - # 3. Build and install pyAMReX (developer install) - cd pyamrex - - # Set environment variable for AMReX source - export AMREX_SRC=$PWD/../amrex - - # Optional: Set compilers explicitly - # export CC=$(which clang) - # export CXX=$(which clang++) - # For GPU support: - # export CUDACXX=$(which nvcc) - # export CUDAHOSTCXX=$(which clang++) - - # Install Python requirements - python3 -m pip install -U -r requirements.txt - python3 -m pip install -v --force-reinstall --no-deps . + # conda create -y --prefix /global/common/software/myproject/$USER/pytuq_integration python=3.11 - # Build with cmake (includes all dimensions) - cmake -S . -B build -DAMReX_SPACEDIM="1;2;3" -DpyAMReX_amrex_src=$(pwd)/../amrex - cmake --build build --target pip_install -j 8 + conda activate pytuq_integration + # For NERSC: conda activate /global/common/software/myproject/$USER/pytuq_integration - cd ../ - - # 4. Install PyTUQ + # 3. Install PyTUQ cd pytuq python -m pip install -r requirements.txt python -m pip install . conda install -y dill cd ../ - # 5. Setup workflow files (optional - for Case 1 examples) - # mkdir rundir - # cd rundir - # tar -xf ~/workflow_uqpc.tar # Obtain from PyTUQ examples - # cd ../ - - # 6. Verify installation - conda list | grep pyamrex # Should show pyamrex 25.10 + # 4. Verify installation conda list | grep pytuq # Should show pytuq 1.0.0z +For a full install including this tutorial, amrex, pyamrex, and pytuq see `example_detailed_install.sh <../../../GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh>`_ + .. note:: For NERSC users, consider placing your conda environment in ``/global/common/software`` @@ -114,15 +75,6 @@ Install the following components in order: `_ for details. -.. warning:: - - Ensure version compatibility: - - - AMReX: 25.10 - - pyAMReX: 25.10 - - PyTUQ: v1.0.0z - - amrex-tutorials: development branch (or jmsexton03/add_pybind_interface_test for testing) - Reference PyTUQ Workflow Examples ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -136,16 +88,16 @@ PyTUQ provides several workflow examples demonstrating the model interface: - Model Interface Pattern * - Global Sensitivity - `ex_pcgsa.py `_ - - ``y = model(x)`` with polynomial chaos + - ``myfunc = Ishigami(); ysam = myfunc(xsam)`` with polynomial chaos * - Inverse Modeling - `ex_mcmc_fitmodel.py `_ - - ``y_pred = model(params)`` for likelihood evaluation + - ``true_model, true_model_params = linear_model, {'W': W, 'b': b}; yd = true_model(true_model_input, true_model_params)`` for likelihood evaluation * - Gaussian Process - `ex_gp.py `_ - - Surrogate: ``y_approx = gp.predict(x)`` + - Surrogate: ``true_model = sin4; y = true_model(x)+datastd*np.random.randn(ntrn)`` * - Linear Regression - `ex_lreg_merr.py `_ - - ``y = X @ beta + error`` + - ``true_model = lambda x: x[:,0]**4 - 2.*x[:,0]**3 #fcb.sin4; y = true_model(x)`` Integration Cases ----------------- @@ -258,7 +210,7 @@ Case 1: C++ with Datalog Output 3. Configure bash wrapper (``model.x``): .. code-block:: bash - :caption: Configuration section of model_wrapper.x + :caption: Configuration section of model.x # Configuration EXE="main3d.gnu.ex" @@ -305,10 +257,10 @@ Case 2: PyAMReX Direct Integration import numpy as np import pyamrex.amrex as amrex - from BaseModel import BaseModel + from AMReXBaseModel import AMReXBaseModel #Define inherited class with __call__ and outnames and pnames methods - class HeatEquationModel(BaseModel) + class HeatEquationModel(AMReXBaseModel) # Direct usage with PyTUQ model = HeatEquationModel() diff --git a/GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh b/GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh new file mode 100644 index 00000000..d57aac3c --- /dev/null +++ b/GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# For NERSC: module load conda + +# 1. Clone repositories +git clone --recursive --branch v1.0.0z https://github.com/sandialabs/pytuq +git clone --branch 25.10 https://github.com/amrex-codes/pyamrex +git clone --branch 25.10 https://github.com/amrex-codes/amrex +git clone --branch development https://github.com/amrex-codes/amrex-tutorials +# Alternative: git clone --branch add_pybind_interface_test https://github.com/jmsexton03/amrex-tutorials + +# 2. Setup conda environment +# Create conda environment (use -y for non-interactive) +conda create -y --name pyamrex_pytuq python=3.11 --no-default-packages + +# For NERSC (see https://docs.nersc.gov/development/languages/python/nersc-python/#moving-your-conda-setup-to-globalcommonsoftware): +# conda create -y --prefix /global/common/software/myproject/$USER/pyamrex_pytuq python=3.11 + +conda activate pyamrex_pytuq +# For NERSC: conda activate /global/common/software/myproject/$USER/pyamrex_pytuq + +# 3. Build and install pyAMReX (developer install) +cd pyamrex + +# Set environment variable for AMReX source +export AMREX_SRC=$PWD/../amrex + +# Optional: Set compilers explicitly +# export CC=$(which clang) +# export CXX=$(which clang++) +# For GPU support: +# export CUDACXX=$(which nvcc) +# export CUDAHOSTCXX=$(which clang++) + +# Install Python requirements +python3 -m pip install -U -r requirements.txt +python3 -m pip install -v --force-reinstall --no-deps . + +# Build with cmake (includes all dimensions) +cmake -S . -B build -DAMReX_SPACEDIM="1;2;3" -DpyAMReX_amrex_src=$(pwd)/../amrex +cmake --build build --target pip_install -j 8 + +cd ../ + +# 4. Install PyTUQ +cd pytuq +python -m pip install -r requirements.txt +python -m pip install . +conda install -y dill +cd ../ + +# 5. Setup workflow files (optional - for Case 1 examples) +# mkdir rundir +# cd rundir +# tar -xf ~/workflow_uqpc.tar # Obtain from PyTUQ examples +# cd ../ + +# 6. Verify installation +conda list | grep pyamrex # Should show pyamrex 25.10 +conda list | grep pytuq # Should show pytuq 1.0.0z From 551a335b9a9d660c754a99a5baba486c2e8d9421 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 6 Oct 2025 15:57:56 -0700 Subject: [PATCH 079/142] Move run management --- GuidedTutorials/HeatEquation_UQ/{Case-2 => Case-1}/model.x | 0 .../HeatEquation_UQ/{Case-2 => Case-1}/postprocess_datalog.sh | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename GuidedTutorials/HeatEquation_UQ/{Case-2 => Case-1}/model.x (100%) rename GuidedTutorials/HeatEquation_UQ/{Case-2 => Case-1}/postprocess_datalog.sh (100%) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/model.x b/GuidedTutorials/HeatEquation_UQ/Case-1/model.x similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/model.x rename to GuidedTutorials/HeatEquation_UQ/Case-1/model.x diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/postprocess_datalog.sh b/GuidedTutorials/HeatEquation_UQ/Case-1/postprocess_datalog.sh similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/postprocess_datalog.sh rename to GuidedTutorials/HeatEquation_UQ/Case-1/postprocess_datalog.sh From 1eeb2363bf4a537b2b5ed7aa7747b2cfb60e70a9 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 6 Oct 2025 16:13:04 -0700 Subject: [PATCH 080/142] Add with subprocess --- .../HeatEquation_UQ/Case-1/AMReXBaseModel.py | 358 ++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py new file mode 100644 index 00000000..9bafb3a1 --- /dev/null +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py @@ -0,0 +1,358 @@ +from pytuq.func.func import ModelWrapperFcn +import numpy as np +from abc import abstractmethod +import os +import tempfile + +from pytuq.func.func import ModelWrapperFcn +import numpy as np +import amrex.space3d as amr + +def load_cupy(): + """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" + if amr.Config.have_gpu: + try: + import cupy as cp + amr.Print("Note: found and will use cupy") + return cp + except ImportError: + amr.Print("Warning: GPU found but cupy not available! Using numpy...") + import numpy as np + return np + if amr.Config.gpu_backend == "SYCL": + amr.Print("Warning: SYCL GPU backend not yet implemented for Python") + import numpy as np + return np + else: + import numpy as np + amr.Print("Note: found and will use numpy") + return np + +class AMReXBaseModel(ModelWrapperFcn): + """Base class for AMReX models with yt-style field info""" + + # Class-level field definitions (to be overridden by subclasses) + _field_info_class = None + _param_fields = [] + _output_fields = [] + _spatial_domain_bounds = None + + # Subprocess configuration + _model_script = './model.x' # Path to model.x wrapper script + _use_subprocess = False # Enable subprocess mode + + def __init__(self, model=None, use_subprocess=False, model_script=None, **kwargs): + # Initialize AMReX if needed + self.xp = load_cupy() + if not amr.initialized(): + amr.initialize([]) + + # Configure subprocess mode + self._use_subprocess = use_subprocess or self.__class__._use_subprocess + if model_script: + self._model_script = model_script + + # Create modelpar from existing parameter information + modelpar = self._create_modelpar() + + # Setup field info container + self.field_info = self._create_field_info() + + # Setup convenience lists + self.param_names = [f[1] for f in self._param_fields] + self.output_names = [f[1] for f in self._output_fields] + + # Extract parameter bounds and create domain array for Function + param_domain = self._extract_param_domain() + + # Determine dimensions + ndim = len(self.param_names) + outdim = len(self.output_names) + + # Create model function wrapper + if model is None: + model_func = lambda params, mp=None: self._run_simulation(params) + else: + model_func = model + + # Initialize Function with model + super().__init__( + model_func, + ndim, + modelpar=modelpar, + name=kwargs.get('name', 'AMReXModel') + ) + + # Set output dimension (ModelWrapperFcn defaults to 1) + self.outdim = outdim + + # Set the parameter domain using Function's method + if param_domain is not None and len(param_domain) > 0: + self.setDimDom(domain=param_domain) + + # Setup spatial domain bounds (yt-style) - separate from parameter bounds + if self._spatial_domain_bounds: + self.domain_left_edge = self._spatial_domain_bounds[0] + self.domain_right_edge = self._spatial_domain_bounds[1] + self.domain_dimensions = (self._spatial_domain_bounds[2] + if len(self._spatial_domain_bounds) > 2 + else None) + + def _create_field_info(self): + """Create yt-style field info container""" + field_info = {} + + for field_tuple in self._param_fields: + field_info[field_tuple] = self._get_field_info(field_tuple) + + for field_tuple in self._output_fields: + field_info[field_tuple] = self._get_field_info(field_tuple) + + return field_info + + def _extract_param_domain(self): + """Extract parameter bounds into Function-compatible domain array""" + domain_list = [] + + for field_tuple in self._param_fields: + info = self._get_field_info(field_tuple) + if 'bounds' in info: + domain_list.append(info['bounds']) + else: + # Use Function's default domain size + dmax = getattr(self, 'dmax', 10.0) + domain_list.append([-dmax, dmax]) + + if domain_list: + return np.array(domain_list) + return None + + def forward(self, x): + """ + PyTorch-compatible forward method for inference. + + Args: + x: torch.Tensor or np.ndarray + Returns: + torch.Tensor if input is tensor, else np.ndarray + """ + # Check if input is PyTorch tensor + is_torch = False + if hasattr(x, 'detach'): # Duck typing for torch.Tensor + is_torch = True + import torch + x_np = x.detach().cpu().numpy() + else: + x_np = x + + # Run simulation using existing logic + outputs = self._run_simulation(x_np) + + # Convert back to torch if needed + if is_torch: + return torch.from_numpy(outputs).to(x.device) + return outputs + + def __call__(self, x): + + if x.ndim == 1: + x = x.reshape(1, -1) + self.checkDim(x) + if hasattr(self, 'domain') and self.domain is not None: + self.checkDomain(x) + + if self.modelpar is None: + outputs = self.model(x) + else: + outputs = self.model(x, self.modelpar) + + if outputs.ndim == 1: + outputs = outputs.reshape(-1, self.outdim) # ← Allows MULTIPLE outputs + + return outputs + + def _run_simulation(self, params): + """ + Core simulation logic - uses subprocess if enabled. + + Args: + params: numpy array of parameters (1D or 2D) + + Returns: + numpy array of outputs + """ + # Ensure params is 2D (n_samples x n_params) + if params.ndim == 1: + params = params.reshape(1, -1) + + n_samples = params.shape[0] + outdim = len(self.output_names) if self.output_names else 1 + + if self._use_subprocess: + # Use model.x subprocess approach + return self._run_subprocess(params) + else: + # Original in-process method + outputs = np.zeros((n_samples, outdim)) + + if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): + for i in range(n_samples): + sim_state = self.evolve(params[i, :]) + outputs[i, :] = self.postprocess(sim_state) + else: + raise NotImplementedError( + "Must implement _run_simulation or evolve/postprocess methods" + ) + return outputs + + def _run_subprocess(self, params): + """ + Run simulation using model.x subprocess (like online_bb example). + + Args: + params: 2D numpy array (n_samples x n_params) + + Returns: + 2D numpy array (n_samples x n_outputs) + """ + # Check if model.x exists + if not os.path.exists(self._model_script): + raise FileNotFoundError(f"Model script not found: {self._model_script}") + + # Create temporary input/output files + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + input_file = f.name + np.savetxt(f, params) + + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + output_file = f.name + + try: + # Run model.x + cmd = f'{self._model_script} {input_file} {output_file}' + print(f"Running: {cmd}") + exit_code = os.system(cmd) + + if exit_code != 0: + raise RuntimeError(f"Command failed with exit code {exit_code}: {cmd}") + + # Load outputs + outputs = np.loadtxt(output_file).reshape(params.shape[0], -1) + + return outputs + + finally: + # Clean up temporary files + for f in [input_file, output_file]: + if os.path.exists(f): + os.unlink(f) + + @property + def field_list(self): + """All available fields""" + return list(self.field_info.keys()) + + def _get_field_info(self, field_tuple): + """ + Override in subclass to provide field metadata. + + Args: + field_tuple: (field_type, field_name) tuple + + Returns: + dict with 'bounds', 'units', 'mean', 'std', etc. + """ + # Default empty implementation + return {} + + def get_param_info(self, param_name): + """Get info for a specific parameter""" + for field_tuple in self._param_fields: + if field_tuple[1] == param_name: + return self._get_field_info(field_tuple) + return {} + + def get_output_info(self, output_name): + """Get info for a specific output""" + for field_tuple in self._output_fields: + if field_tuple[1] == output_name: + return self._get_field_info(field_tuple) + return {} + + def _create_modelpar(self): + """Create modelpar dictionary with statistical properties""" + modelpar = { + 'param_info': {}, + 'output_info': {}, + 'param_names': [], + 'output_names': [], + 'defaults': {}, + 'bounds': {}, + 'units': {}, + 'mean': [], + 'std': [], + 'distribution': [], # 'normal', 'uniform', 'lognormal', etc. + 'pc_type': 'HG', # Hermite-Gaussian by default + } + + # Extract parameter information including statistical properties + for field_tuple in self._param_fields: + field_type, field_name = field_tuple + info = self._get_field_info(field_tuple) + + modelpar['param_info'][field_name] = info + modelpar['param_names'].append(field_name) + + # Extract statistical properties + if 'mean' in info: + modelpar['mean'].append(info['mean']) + elif 'default' in info: + modelpar['mean'].append(info['default']) + else: + # Use center of bounds if available + if 'bounds' in info: + modelpar['mean'].append(np.mean(info['bounds'])) + else: + modelpar['mean'].append(0.0) + + if 'std' in info: + modelpar['std'].append(info['std']) + else: + # Default to 10% of mean or range + if 'bounds' in info: + # Use range/6 as rough std (99.7% within bounds) + modelpar['std'].append((info['bounds'][1] - info['bounds'][0])/6.0) + else: + modelpar['std'].append(abs(modelpar['mean'][-1]) * 0.1) + + # Store other properties + if 'bounds' in info: + modelpar['bounds'][field_name] = info['bounds'] + if 'units' in info: + modelpar['units'][field_name] = info['units'] + if 'distribution' in info: + modelpar['distribution'].append(info['distribution']) + else: + modelpar['distribution'].append('normal') # default + + # Convert to numpy arrays for easier manipulation + modelpar['mean'] = np.array(modelpar['mean']) + modelpar['std'] = np.array(modelpar['std']) + + # Add output information + for field_tuple in self._output_fields: + field_type, field_name = field_tuple + info = self._get_field_info(field_tuple) + modelpar['output_info'][field_name] = info + modelpar['output_names'].append(field_name) + + return modelpar + + def write_param_marginals(self, filename='param_margpc.txt'): + """Write parameter marginals file for PC analysis""" + with open(filename, 'w') as f: + for i, name in enumerate(self.modelpar['param_names']): + mean = self.modelpar['mean'][i] + std = self.modelpar['std'][i] + f.write(f"{mean} {std}\n") + print(f"Wrote parameter marginals to {filename}") From 5aa1ca21585f09792e483da35206b3ad47503670 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 6 Oct 2025 16:40:59 -0700 Subject: [PATCH 081/142] Runs in ex_pcgsa, plots fail --- .../HeatEquation_UQ/Case-1/ModelXBaseModel.py | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 GuidedTutorials/HeatEquation_UQ/Case-1/ModelXBaseModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/ModelXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-1/ModelXBaseModel.py new file mode 100644 index 00000000..a5bd5975 --- /dev/null +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/ModelXBaseModel.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +import numpy as np +from typing import Tuple, List, Optional +import amrex.space3d as amr +from AMReXBaseModel import AMReXBaseModel, load_cupy + + +class HeatEquationModel(AMReXBaseModel): + """ + Heat equation simulation model with full statistical properties + and AMReX integration. + """ + + # Define parameter fields with proper naming convention + _param_fields = [ + ('param', 'diffusion_coeff'), + ('param', 'init_amplitude'), + ('param', 'init_width'), + ] + + # Define output fields + _output_fields = [ + ('output', 'max_temp'), + ('output', 'mean_temp'), + ('output', 'std_temp'), + ('output', 'total_energy'), + ] + + # Spatial domain bounds (3D heat equation domain) + _spatial_domain_bounds = [ + np.array([0.0, 0.0, 0.0]), # left edge + np.array([1.0, 1.0, 1.0]), # right edge + np.array([32, 32, 32]) # default grid dimensions + ] + + # Enable subprocess mode + _use_subprocess = True + _model_script = './model.x' + + def _get_field_info(self, field_tuple): + """ + Provide field metadata with statistical properties. + Only includes fields that are actually used by the framework. + """ + field_type, field_name = field_tuple + + field_info_dict = { + # Parameters - match param_margpc.txt values + ('param', 'diffusion_coeff'): { + 'mean': 1.0, + 'std': 0.25, + 'bounds': [0.1, 5.0], + 'distribution': 'normal', + 'units': 'm**2/s', + 'display_name': 'Thermal Diffusivity', + }, + ('param', 'init_amplitude'): { + 'mean': 1.0, + 'std': 0.25, + 'bounds': [0.1, 3.0], + 'distribution': 'normal', + 'units': 'K', + 'display_name': 'Initial Temperature', + }, + ('param', 'init_width'): { + 'mean': 0.01, + 'std': 0.0025, + 'bounds': [0.001, 0.1], + 'distribution': 'normal', + 'units': 'm', + 'display_name': 'Initial Width', + }, + + # Outputs - just units and display names + ('output', 'max_temp'): { + 'units': 'K', + 'display_name': 'Maximum Temperature', + }, + ('output', 'mean_temp'): { + 'units': 'K', + 'display_name': 'Mean Temperature', + }, + ('output', 'std_temp'): { + 'units': 'K', + 'display_name': 'Temperature Std Dev', + }, + ('output', 'total_energy'): { + 'units': 'J', + 'display_name': 'Total Energy', + }, + } + + return field_info_dict.get(field_tuple, {}) From a739f28785b81fb5f984df6cbdbe715d3c031e9e Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 7 Oct 2025 07:44:54 -0700 Subject: [PATCH 082/142] Respect outdim if it's overridden, tweak bounds --- GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py | 7 ++++--- GuidedTutorials/HeatEquation_UQ/Case-1/ModelXBaseModel.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py index 9bafb3a1..95306702 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py @@ -186,11 +186,11 @@ def _run_simulation(self, params): params = params.reshape(1, -1) n_samples = params.shape[0] - outdim = len(self.output_names) if self.output_names else 1 +# outdim = len(self.output_names) if self.output_names else 1 if self._use_subprocess: # Use model.x subprocess approach - return self._run_subprocess(params) + outputs = self._run_subprocess(params) else: # Original in-process method outputs = np.zeros((n_samples, outdim)) @@ -203,7 +203,8 @@ def _run_simulation(self, params): raise NotImplementedError( "Must implement _run_simulation or evolve/postprocess methods" ) - return outputs + # Ensure we only return outdim outputs + return outputs[:, :self.outdim] def _run_subprocess(self, params): """ diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/ModelXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-1/ModelXBaseModel.py index a5bd5975..171fc6c6 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/ModelXBaseModel.py +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/ModelXBaseModel.py @@ -49,7 +49,7 @@ def _get_field_info(self, field_tuple): ('param', 'diffusion_coeff'): { 'mean': 1.0, 'std': 0.25, - 'bounds': [0.1, 5.0], + 'bounds': [0.25, 1.75], # mean ± 3*std 'distribution': 'normal', 'units': 'm**2/s', 'display_name': 'Thermal Diffusivity', @@ -57,7 +57,7 @@ def _get_field_info(self, field_tuple): ('param', 'init_amplitude'): { 'mean': 1.0, 'std': 0.25, - 'bounds': [0.1, 3.0], + 'bounds': [0.25, 1.75], # mean ± 3*std 'distribution': 'normal', 'units': 'K', 'display_name': 'Initial Temperature', @@ -65,7 +65,7 @@ def _get_field_info(self, field_tuple): ('param', 'init_width'): { 'mean': 0.01, 'std': 0.0025, - 'bounds': [0.001, 0.1], + 'bounds': [0.0025, 0.0175], # mean ± 3*std 'distribution': 'normal', 'units': 'm', 'display_name': 'Initial Width', From 9f1f36d37317819ea70d4805bf74114d3433a5e7 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 7 Oct 2025 08:05:51 -0700 Subject: [PATCH 083/142] Make outdim use self --- GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py | 4 ++-- GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py index 95306702..fd64934b 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py @@ -186,7 +186,7 @@ def _run_simulation(self, params): params = params.reshape(1, -1) n_samples = params.shape[0] -# outdim = len(self.output_names) if self.output_names else 1 + outdim = getattr(self, 'outdim', len(getattr(self, 'output_names', [])) or 1) if self._use_subprocess: # Use model.x subprocess approach @@ -204,7 +204,7 @@ def _run_simulation(self, params): "Must implement _run_simulation or evolve/postprocess methods" ) # Ensure we only return outdim outputs - return outputs[:, :self.outdim] + return outputs[:, :outdim] def _run_subprocess(self, params): """ diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py index d060ed33..f35ecc6e 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py @@ -175,7 +175,7 @@ def _run_simulation(self, params): params = params.reshape(1, -1) n_samples = params.shape[0] - outdim = len(self.output_names) if self.output_names else 1 + outdim = getattr(self, 'outdim', len(getattr(self, 'output_names', [])) or 1) outputs = np.zeros((n_samples, outdim)) # Check if subclass has evolve/postprocess methods @@ -190,7 +190,7 @@ def _run_simulation(self, params): "Must implement _run_simulation or evolve/postprocess methods" ) - return outputs + return outputs[:, :outdim] @property def field_list(self): From 537742c2205800361f37d4667a74fba3231c8d25 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 7 Oct 2025 15:12:01 -0700 Subject: [PATCH 084/142] Make requested outputs and pyamrex use consistent --- Docs/source/HeatEquation_PythonDriver.rst | 453 ------------------ .../HeatEquation_UQ/Case-1/AMReXBaseModel.py | 61 ++- .../HeatEquation_UQ/Case-2/AMReXBaseModel.py | 12 +- 3 files changed, 46 insertions(+), 480 deletions(-) delete mode 100644 Docs/source/HeatEquation_PythonDriver.rst diff --git a/Docs/source/HeatEquation_PythonDriver.rst b/Docs/source/HeatEquation_PythonDriver.rst deleted file mode 100644 index 8fb99945..00000000 --- a/Docs/source/HeatEquation_PythonDriver.rst +++ /dev/null @@ -1,453 +0,0 @@ -.. _guided_heat_python_driver: - -Tutorial: Heat Equation - Python Driver -======================================== - -.. admonition:: **Time to Complete**: 10 mins - :class: warning - - **PREREQUISITES:** - - Complete :ref:`guided_heat_simple` tutorial first - - **GOALS:** - - Create minimal Python interface to AMReX C++ code - - Implement one-line simulation execution from Python - - Access simulation results programmatically - - Understand the function refactoring pattern - - -This tutorial demonstrates two complementary approaches for creating Python interfaces -to AMReX C++ simulation codes: - -- **Case-1**: Minimal pybind11 interface with one-line simulation execution -- **Case-2**: Pure Python approach using pyamrex MultiFabs directly - -Both approaches can handle complex simulations and MultiFab data structures. - -The One-Line Interface -~~~~~~~~~~~~~~~~~~~~~~ - -The goal is to enable simple Python usage patterns that work across different simulation types: - -**Case-1 Pattern (pybind11 C++ interface):** - -.. code-block:: python - - import amrex_sim - - # One-line simulation execution with structured results - result = amrex_sim.run_simulation(["./YourSimulation", "inputs"]) - - print(f"Max value: {result.max_value}") - print(f"Final time: {result.final_time}") - print(f"Success: {result.success}") - -**Case-2 Pattern (Pure Python with pyamrex):** - -.. code-block:: python - - from YourModel import SimulationModel - - # One-line execution with parameter arrays - model = SimulationModel(use_parmparse=True) - results = model(params) # Returns numpy array of results - - print(f"Results: {results}") # [max, mean, std, integral, center] - -Both approaches provide direct access to simulation results without subprocess overhead -or complex callback systems. - -Key Architecture Pattern -~~~~~~~~~~~~~~~~~~~~~~~~ - -Both approaches demonstrate the core principle of creating **simplified interfaces** to complex AMReX simulations. The key insight is to transform existing simulation logic into easily callable functions with structured return values: - -**Case-1 Approach (C++ Refactoring):** -Refactor the C++ ``main()`` function into a reusable function that can be called from both command line and Python: - -.. code-block:: cpp - - // Reusable simulation function - SimulationResult simulation_main(int argc, char* argv[]) { - amrex::Initialize(argc, argv); - { - // All simulation logic here - // ... - result.max_value = multifab.max(0); - result.success = true; - } - amrex::Finalize(); - return result; - } - -**Case-2 Approach (Python Function Design):** -Create a callable class or function that encapsulates simulation parameters and execution: - -.. code-block:: python - - class SimulationModel: - def __call__(self, params): - # All simulation logic using pyamrex - # ... - return np.array([max_val, mean_val, std_val, integral, center_val]) - -Both patterns enable the same core benefit: **one-line simulation execution** with structured results, regardless of whether the underlying implementation uses C++ bindings or pure Python. - -Two Implementation Approaches -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Case-1: Minimal pybind11 Interface -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Location: :code:`amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/Case-1/` - -This approach creates C++ bindings using pybind11 for direct simulation execution: - -.. code-block:: bash - - cd Case-1 - mkdir build && cd build - cmake .. - cmake --build . -j4 - -Creates both: -- ``HeatEquation_PythonDriver`` - C++ executable -- ``amrex_heat.cpython-*.so`` - Python module - -.. note:: - The GNUmakefile in Case-1 is experimental and under development as an alternative build system. - -Case-2: Pure Python with pyamrex -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Location: :code:`amrex-tutorials/GuidedTutorials/HeatEquation_PythonDriver/Case-2/` - -This approach uses pure Python with pyamrex to access MultiFabs and AMReX functionality directly: - -.. code-block:: bash - - cd Case-2 - python HeatEquationModel.py - -Case-1 Files (pybind11 Approach) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``Case-1`` directory contains: - -- ``main.cpp`` - Heat equation solver with ``heat_equation_main()`` function -- ``bindings.cpp`` - Minimal pybind11 interface exposing the one-liner -- ``CMakeLists.txt`` - Build configuration with pybind11 support -- ``GNUmakefile`` - Experimental GNU Make build system (under development) -- ``pybind11.cmake`` - Pybind11 infrastructure (copied from pyamrex) -- ``inputs`` - Simulation parameters (``n_cell``, ``dt``, etc.) -- ``test.py`` - Example Python usage script -- ``README.md`` - Documentation and usage instructions - -Case-2 Files (Pure Python Approach) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The ``Case-2`` directory contains: - -- ``HeatEquationModel.py`` - Pure Python implementation using pyamrex MultiFabs -- ``inputs`` - Simulation parameters -- ``README.md`` - Documentation for the pure Python approach - -Case-1 Implementation Details -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Result Structure -^^^^^^^^^^^^^^^^ - -The simulation returns a simple struct with essential information: - -.. code-block:: cpp - - struct SimulationResult { - double max_temperature; - int final_step; - double final_time; - bool success; - }; - -This struct is automatically exposed to Python through pybind11, allowing direct -access to all fields. - - Minimal Python Bindings - ++++++++++++++++++++++++ - -The Python interface is implemented with minimal pybind11 code in ``bindings.cpp``. -The key components are: - -1. **Forward declaration** of the ``SimulationResult`` struct from ``main.cpp`` -2. **Function declaration** for ``heat_equation_main()`` -3. **Pybind11 module** that exposes both the struct and function - -.. code-block:: cpp - - // Forward declarations from main.cpp - struct SimulationResult { - double max_temperature; - int final_step; - double final_time; - bool success; - }; - - SimulationResult heat_equation_main(int argc, char* argv[]); - - PYBIND11_MODULE(amrex_heat, m) { - m.doc() = "Minimal AMReX Heat Equation Python Interface"; - - // Expose SimulationResult struct - py::class_(m, "SimulationResult") - .def_readonly("max_temperature", &SimulationResult::max_temperature) - .def_readonly("final_step", &SimulationResult::final_step) - .def_readonly("final_time", &SimulationResult::final_time) - .def_readonly("success", &SimulationResult::success); - - // Main simulation function - one-liner interface - m.def("run_simulation", [](py::list args) { - // Convert Python list to C++ argc/argv with proper lifetime management - std::vector args_str; - for (auto item : args) { - args_str.push_back(py::str(item)); - } - - std::vector args_cstr; - for (auto& s : args_str) { - args_cstr.push_back(const_cast(s.c_str())); - } - args_cstr.push_back(nullptr); // Null terminate - - return heat_equation_main(static_cast(args_cstr.size() - 1), args_cstr.data()); - }, "Run the heat equation simulation and return results"); - } - -The argument conversion ensures proper lifetime management of the C++ strings and -null-terminates the argument array as expected by ``argc/argv`` conventions. - - CMake Integration - +++++++++++++++++ - -The ``CMakeLists.txt`` integrates pybind11 using the pyamrex infrastructure. The key -elements are the pybind11 integration and building both targets from the same source: - -.. code-block:: cmake - - # Use pyamrex pybind11 infrastructure - include(pybind11.cmake) - - # Add the main executable - add_executable(HeatEquation_PythonDriver main.cpp) - - # Add the pybind11 module including main simulation logic - pybind11_add_module(amrex_heat bindings.cpp main.cpp) - - # Link AMReX to both targets - target_link_libraries(HeatEquation_PythonDriver PRIVATE AMReX::amrex) - target_link_libraries(amrex_heat PRIVATE AMReX::amrex) - -The ``pybind11.cmake`` file is copied from the pyamrex repository and provides the necessary -pybind11 infrastructure without requiring a separate pyamrex installation. - -Running the Examples -~~~~~~~~~~~~~~~~~~~~ - -Case-1: pybind11 Interface -^^^^^^^^^^^^^^^^^^^^^^^^^^ - -**C++ Executable:** - -.. code-block:: bash - - cd Case-1/build - ./HeatEquation_PythonDriver inputs - -**Python Interface:** - -.. code-block:: bash - - cd Case-1/build - python ../test.py - -Case-2: Pure Python with pyamrex -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -**Pure Python Execution:** - -.. code-block:: bash - - cd Case-2 - python HeatEquationModel.py - -The Case-1 Python script demonstrates accessing simulation results: - -.. code-block:: python - - import amrex_heat - - print("Running simulation...") - result = amrex_heat.run_simulation(["./HeatEquation_PythonDriver", "inputs"]) - - print(f"Simulation Results:") - print(f" Success: {result.success}") - print(f" Final step: {result.final_step}") - print(f" Final time: {result.final_time:.6f}") - print(f" Max temperature: {result.max_temperature:.6f}") - -Expected output: - -.. code-block:: - - Heat Equation Python Driver Test - ======================================== - Running simulation... - Advanced step 1 - Advanced step 2 - ... - Advanced step 1000 - - Simulation Results: - Success: True - Final step: 1000 - Final time: 0.010000 - Max temperature: 1.089070 - - ✓ Simulation completed successfully! - -Case-2 Example Output -^^^^^^^^^^^^^^^^^^^^^ - -The pure Python approach provides object-oriented access: - -.. code-block:: python - - import numpy as np - from HeatEquationModel import HeatEquationModel - - # Create model using inputs file - model = HeatEquationModel(use_parmparse=True) - - # Run with parameter array [diffusion_coeff, init_amplitude, init_width] - params = np.array([1.0, 1.0, 0.01]) - results = model(params) - - print(f"Results: {results}") - # Output: [max_value, mean_value, std_dev, total_heat, center_value] - -Choosing the Right Approach -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Use Case-1 (pybind11) when:** -- You want to create custom bindings for existing C++ code -- You need a one-line interface wrapping complex C++ simulation logic -- You want both command-line and Python interfaces from the same codebase -- You prefer minimal changes to existing C++ code - -**Use Case-2 (Pure Python with pyamrex) when:** -- You want to write simulation logic directly in Python -- You prefer leveraging the full pyamrex ecosystem -- You want object-oriented simulation management -- You need rapid prototyping and development - -Adapting These Patterns -^^^^^^^^^^^^^^^^^^^^^^^ - -**For Case-1 (pybind11 approach):** - -1. **Refactor main()**: Extract simulation logic into reusable function -2. **Design result structure**: Choose what data to return to Python -3. **Create bindings**: Write ``bindings.cpp`` for your interface -4. **Update build**: Add pybind11 support to CMakeLists.txt - -**For Case-2 (Pure Python approach):** - -1. **Design class interface**: Define methods for initialization, execution, results -2. **Use pyamrex directly**: Leverage MultiFabs and AMReX functionality -3. **Implement simulation logic**: Write algorithm using pyamrex primitives -4. **Add data management**: Handle input/output and state management - -Benefits Comparison -~~~~~~~~~~~~~~~~~~~ - -**Case-1 (pybind11) Benefits:** - -- **Minimal C++ changes**: Preserves existing simulation logic -- **Performance**: Direct C++ calls with no overhead -- **Dual interface**: Same code for command line and Python -- **Integration**: Works with existing build systems - -**Case-2 (Pure Python) Benefits:** - -- **Development speed**: Rapid iteration and testing -- **Ecosystem access**: Full pyamrex functionality available -- **Readability**: Clear Python simulation logic -- **Flexibility**: Easy to modify and extend algorithms - -**Both approaches:** - -- Support complex MultiFab operations -- Enable sophisticated simulation workflows -- Integrate well with scientific Python ecosystem -- Provide foundation for advanced features - -Use Cases for Each Approach -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Case-1 (pybind11) is ideal for:** -- **Wrapping existing C++ codes**: Minimal changes to proven simulation codes -- **Performance-critical workflows**: Direct C++ execution with minimal overhead -- **One-line interfaces**: Simple Python access to complex simulations -- **Hybrid development**: Teams with both C++ and Python expertise - -**Case-2 (Pure Python with pyamrex) is ideal for:** -- **Rapid prototyping**: Quick iteration on simulation algorithms -- **Educational purposes**: Clear, readable simulation logic -- **Python-first development**: Teams primarily working in Python -- **Leveraging pyamrex ecosystem**: Using existing pyamrex tools and patterns - -**Both approaches support:** -- Parameter sweeps and optimization workflows -- Jupyter notebooks and interactive visualization -- Complex MultiFab operations and data analysis -- Machine learning and data science pipelines - -Next Steps -~~~~~~~~~~ - -This minimal Python interface provides the foundation for more advanced features: - -1. **Add more return data**: Include arrays, MultiFab statistics, etc. -2. **Parameter setting**: Allow modification of simulation parameters from Python -3. **Progress monitoring**: Add callback system for real-time updates -4. **Full pyamrex integration**: Access MultiFab data structures directly -5. **Workflow automation**: Build complex simulation pipelines -6. **Generic naming**: Replace heat equation-specific names (``amrex_heat``, ``max_temperature``) with generic equivalents (``amrex_sim``, ``max_value``) for reusability across different simulation types -7. **Numpy-compatible results**: Add options to return data as dictionaries, numpy arrays, or other formats that integrate well with the scientific Python ecosystem - -Potential improvements for generic usage: - -.. code-block:: cpp - - // Generic module and function names - PYBIND11_MODULE(amrex_sim, m) { - // Option 1: Return as dictionary for numpy compatibility - m.def("run_dict", [](py::list args) { - auto result = simulation_main(argc, argv); - py::dict d; - d["success"] = result.success; - d["max_value"] = result.max_value; // Generic field name - d["final_time"] = result.final_time; - return d; - }); - - // Option 2: Return numerical data as numpy array - m.def("run_array", [](py::list args) { - auto result = simulation_main(argc, argv); - py::array_t data = py::array_t(3); - // Fill array with [final_step, final_time, max_value] - return py::make_tuple(result.success, data); - }); - } - -The key insight is that this simple pattern scales naturally to support more -complex use cases while maintaining the clean one-line interface. \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py index fd64934b..394e8ecc 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py @@ -6,27 +6,27 @@ from pytuq.func.func import ModelWrapperFcn import numpy as np -import amrex.space3d as amr - -def load_cupy(): - """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" - if amr.Config.have_gpu: - try: - import cupy as cp - amr.Print("Note: found and will use cupy") - return cp - except ImportError: - amr.Print("Warning: GPU found but cupy not available! Using numpy...") - import numpy as np - return np - if amr.Config.gpu_backend == "SYCL": - amr.Print("Warning: SYCL GPU backend not yet implemented for Python") - import numpy as np - return np - else: - import numpy as np - amr.Print("Note: found and will use numpy") - return np +#import amrex.space3d as amr + +#def load_cupy(): +# """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" +# if amr.Config.have_gpu: +# try: +# import cupy as cp +# amr.Print("Note: found and will use cupy") +# return cp +# except ImportError: +# amr.Print("Warning: GPU found but cupy not available! Using numpy...") +# import numpy as np +# return np +# if amr.Config.gpu_backend == "SYCL": +# amr.Print("Warning: SYCL GPU backend not yet implemented for Python") +# import numpy as np +# return np +# else: +# import numpy as np +# amr.Print("Note: found and will use numpy") +# return np class AMReXBaseModel(ModelWrapperFcn): """Base class for AMReX models with yt-style field info""" @@ -35,6 +35,8 @@ class AMReXBaseModel(ModelWrapperFcn): _field_info_class = None _param_fields = [] _output_fields = [] + # Requested output fields (defaults to all outputs) + _request_out_fields = None _spatial_domain_bounds = None # Subprocess configuration @@ -43,9 +45,9 @@ class AMReXBaseModel(ModelWrapperFcn): def __init__(self, model=None, use_subprocess=False, model_script=None, **kwargs): # Initialize AMReX if needed - self.xp = load_cupy() - if not amr.initialized(): - amr.initialize([]) +# self.xp = load_cupy() +# if not amr.initialized(): +# amr.initialize([]) # Configure subprocess mode self._use_subprocess = use_subprocess or self.__class__._use_subprocess @@ -203,8 +205,15 @@ def _run_simulation(self, params): raise NotImplementedError( "Must implement _run_simulation or evolve/postprocess methods" ) - # Ensure we only return outdim outputs - return outputs[:, :outdim] + # Return only requested outputs if specified + requested = self._request_out_fields or self._output_fields + if requested != self._output_fields: + all_names = [field[1] for field in self._output_fields] + requested_names = [field[1] for field in requested] + indices = [all_names.index(name) for name in requested_names] + return outputs[:, indices] + + return outputs def _run_subprocess(self, params): """ diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py index f35ecc6e..c322fa6c 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py @@ -33,6 +33,8 @@ class AMReXBaseModel(ModelWrapperFcn): _field_info_class = None _param_fields = [] _output_fields = [] + # Requested output fields (defaults to all outputs) + _request_out_fields = None _spatial_domain_bounds = None def __init__(self, model=None, **kwargs): @@ -190,7 +192,15 @@ def _run_simulation(self, params): "Must implement _run_simulation or evolve/postprocess methods" ) - return outputs[:, :outdim] + # Return only requested outputs if specified + requested = self._request_out_fields or self._output_fields + if requested != self._output_fields: + all_names = [field[1] for field in self._output_fields] + requested_names = [field[1] for field in requested] + indices = [all_names.index(name) for name in requested_names] + return outputs[:, indices] + + return outputs @property def field_list(self): From 5e9ca5fef66da785b820df4625fc2e655973cd1c Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 7 Oct 2025 15:19:33 -0700 Subject: [PATCH 085/142] Style --- GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py | 8 ++++---- .../HeatEquation_UQ/Case-1/postprocess_datalog.sh | 2 +- GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py index 394e8ecc..a5c20bda 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py @@ -196,7 +196,7 @@ def _run_simulation(self, params): else: # Original in-process method outputs = np.zeros((n_samples, outdim)) - + if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): for i in range(n_samples): sim_state = self.evolve(params[i, :]) @@ -212,7 +212,7 @@ def _run_simulation(self, params): requested_names = [field[1] for field in requested] indices = [all_names.index(name) for name in requested_names] return outputs[:, indices] - + return outputs def _run_subprocess(self, params): @@ -242,13 +242,13 @@ def _run_subprocess(self, params): cmd = f'{self._model_script} {input_file} {output_file}' print(f"Running: {cmd}") exit_code = os.system(cmd) - + if exit_code != 0: raise RuntimeError(f"Command failed with exit code {exit_code}: {cmd}") # Load outputs outputs = np.loadtxt(output_file).reshape(params.shape[0], -1) - + return outputs finally: diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/postprocess_datalog.sh b/GuidedTutorials/HeatEquation_UQ/Case-1/postprocess_datalog.sh index eee8b6e8..794807f7 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/postprocess_datalog.sh +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/postprocess_datalog.sh @@ -24,7 +24,7 @@ fi if [ "$RUN_COUNTER" -eq 0 ] && [ ! -f "$OUTNAMES_FILE" ]; then # Extract header line, remove #, clean up spaces header=$(grep '^#' "$DATALOG" | head -1 | sed 's/^#//' | tr -s ' ' | sed 's/^ *//' | sed 's/ *$//') - + if [ -n "$header" ]; then echo "$header" | tr ' ' '\n' > "$OUTNAMES_FILE" echo "Generated $OUTNAMES_FILE from datalog header" >&2 diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py index c322fa6c..0cd3fdd9 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py @@ -199,7 +199,7 @@ def _run_simulation(self, params): requested_names = [field[1] for field in requested] indices = [all_names.index(name) for name in requested_names] return outputs[:, indices] - + return outputs @property From c4cacad84ff2fc64ca6aa4e8eeedfd82b60d34e2 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 7 Oct 2025 15:44:40 -0700 Subject: [PATCH 086/142] Shorter outline, leave first draft at end --- .github/workflows/docs.yml | 2 +- Docs/source/HeatEquation_UQ.rst | 249 ++++++++++++++++++++++++++------ Docs/source/conf.py | 5 + 3 files changed, 212 insertions(+), 44 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2f0055ac..b3c2a68a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: .github/workflows/dependencies/documentation.sh echo "Installing python packages for docs..." python3 -m pip install --upgrade pip - python3 -m pip install sphinx sphinx_rtd_theme breathe sphinxcontrib.bibtex docutils + python3 -m pip install sphinx sphinx_rtd_theme breathe sphinxcontrib.bibtex docutils sphinx-copybutton - name: Install and Build run: | diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 7c12f908..8ed2912e 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -1,7 +1,170 @@ .. _guided_pytuq_integration: -PyTUQ Integration with AMReX Applications -========================================== +.. _pytuq_quickstart: + +PyTUQ Quick Start Guide +======================= + +.. admonition:: **Time to Complete**: 15-20 minutes + :class: note + + **What you will learn**: + - Install PyTUQ + - Run AMReX + PyTUQ examples + - Deploy on Perlmutter + +Installation +------------ + +.. code-block:: bash + :caption: Quick install + + git clone --recursive --branch v1.0.0z https://github.com/sandialabs/pytuq + cd pytuq + echo "dill" >> requirements.txt + pip install -r requirements.txt + pip install . + +Examples +-------- + +C++ AMReX + PyTUQ +~~~~~~~~~~~~~~~~~ + +.. admonition:: Build and Run + :class: dropdown + + **Prerequisites**: AMReX compiled with MPI support + + .. code-block:: bash + :caption: Build C++ example + + cd GuidedTutorials/HeatEquation_UQ/Case-1 + make -j4 + + .. code-block:: bash + :caption: Copy ex_pcgsa.py and use the black box model + + cp ../../../../pytuq/examples/ex_pcgsa.py + # replace Ishigami() with minimal python model + + 16d15 + < from ModelXBaseModel import HeatEquationModel + 23,24c22 + < myfunc = HeatEquationModel() + < myfunc._request_out_fields = [('output', 'max_temp')] + --- + > myfunc = Ishigami() + + .. code-block:: bash + :caption: Run with Python wrapped bash + + python ./ex_pcgsa.py + +PyAMReX + PyTUQ +~~~~~~~~~~~~~~~ + +.. admonition:: Setup and Run + :class: dropdown + + **Prerequisites**: pyAMReX installed + + .. code-block:: bash + :caption: Direct Python integration + + cp ../../../../pytuq/examples/ex_pcgsa.py + # replace Ishigami() with pyamrex model + + 16d15 + < from HeatEquationModel import HeatEquationModel + 23,24c22 + < myfunc = HeatEquationModel() + < myfunc._request_out_fields = [('output', 'max_temp')] + --- + > myfunc = Ishigami() + + .. code-block:: bash + :caption: Run + + python ./ex_pcgsa.py + +Perlmutter Deployment (not implemented) +--------------------------------------- + +.. note:: + + Module setup required: ``module load python cuda`` + +C++ AMReX on Perlmutter +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. admonition:: Perlmutter Setup + :class: dropdown + + .. code-block:: bash + :caption: perlmutter_build.sh + + module load PrgEnv-gnu cuda + make USE_MPI=TRUE USE_CUDA=TRUE + + .. code-block:: bash + :caption: Submit job + + sbatch run_amrex_uq.slurm + +PyAMReX on Perlmutter +~~~~~~~~~~~~~~~~~~~~~ + +.. admonition:: Perlmutter Setup + :class: dropdown + + .. code-block:: bash + :caption: Virtual environment setup + + module load conda + # For NERSC (see https://docs.nersc.gov/development/languages/python/nersc-python/#moving-your-conda-setup-to-globalcommonsoftware): + conda create -y --prefix /global/common/software/myproject/$USER/pytuq_integration python=3.11 + + + .. code-block:: bash + :caption: Run PyAMReX + PyTUQ + + srun -n 4 python run_pyamrex_uq.py + + .. note:: + + For NERSC users, consider placing your conda environment in ``/global/common/software`` + for better performance and persistence. See the `NERSC Python documentation + `_ + for details. + +Summary +------- + +**Key Takeaways:** + +* PyTUQ can use models with ``outputs = model(inputs)`` interface +* C++ codes need wrapper scripts; Python codes integrate directly +* Best practices for Perlmutter requires environment modules and MPI configuration + +Additional Resources +-------------------- + +- `PyTUQ Documentation `_ +- `PyTUQ Examples directory `_ +- `AMReX Documentation `_ +- `pyAMReX Documentation `_ +- :ref:`_guided_heat` - Base tutorial this builds upon + +.. seealso:: + + For complete working examples of the ``outputs = model(inputs)`` pattern, see: + + - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-1/`` - C++ wrappers + - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-2/`` - PyAMReX native + +PyTUQ Integration with AMReX Applications (First draft) +======================================================= .. admonition:: **Time to Complete**: 30-45 minutes :class: note @@ -10,7 +173,7 @@ PyTUQ Integration with AMReX Applications - Basic knowledge of AMReX build system - Familiarity with Python/NumPy - Understanding of uncertainty quantification concepts - + **What you will learn**: - How to wrap AMReX simulations with PyTUQ's generic model interface - Converting simulation codes to ``outputs = model(inputs)`` pattern @@ -40,39 +203,39 @@ Install pytuq as described in `pytuq/README.md `_ - + .. note:: - For NERSC users, consider placing your conda environment in ``/global/common/software`` - for better performance and persistence. See the `NERSC Python documentation - `_ + For NERSC users, consider placing your conda environment in ``/global/common/software`` + for better performance and persistence. See the `NERSC Python documentation + `_ for details. Reference PyTUQ Workflow Examples @@ -110,32 +273,32 @@ Case 1: C++ Application Wrappers 1. Add physics parameters to main.cpp declarations: .. code-block:: cpp - + amrex::Real diffusion_coeff; - amrex::Real init_amplitude; + amrex::Real init_amplitude; amrex::Real init_width; 2. Read parameters from inputs file: .. code-block:: cpp - + diffusion_coeff = 1.0; pp.query("diffusion_coeff", diffusion_coeff); - + init_amplitude = 1.0; pp.query("init_amplitude", init_amplitude); - + init_width = 0.01; pp.query("init_width", init_width); 3. Use parameters in initial conditions and evolution: .. code-block:: cpp - + // Initial conditions amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); - + // Evolution phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; @@ -143,9 +306,9 @@ Case 1: C++ Application Wrappers The key to PyTUQ integration is creating a loop that maps input parameters to output quantities. This loop structure differs between cases: - + - **Case 1**: Bash workflow manages run directories, runs executable with input parameters, parses outputs - - **Case 2**: Python loop directly calls pyAMReX functions + - **Case 2**: Python loop directly calls pyAMReX functions The simplest output extraction method is described here: @@ -162,7 +325,7 @@ Case 1: C++ with Datalog Output 1. Add datalog configuration to C++ code: .. code-block:: cpp - + const int datwidth = 14; const int datprecision = 6; int datalog_int = -1; @@ -172,7 +335,7 @@ Case 1: C++ with Datalog Output 2. Write statistics to datalog.txt: .. code-block:: cpp - + // Check if we should write datalog bool write_datalog = false; if (datalog_final && step == nsteps) { @@ -180,27 +343,27 @@ Case 1: C++ with Datalog Output } else if (datalog_int > 0 && step % datalog_int == 0) { write_datalog = true; // Write every datalog_int steps } - + if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { std::ofstream datalog("datalog.txt", std::ios::app); - + amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); amrex::Real max_temperature = phi_new.max(0); amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; - + datalog << time << " " << max_temperature << " " << std_temperature << " " << step << std::endl; } .. note:: - + Some AMReX codes already have datalog capabilities: - + - For existing datalogs, you may only need to ``tail -n 1 datalog`` for the final values - For AMR codes, use the built-in DataLog: - + .. code-block:: cpp - + // Assuming 'amr' is your Amr object if (amrex::ParallelDescriptor::IOProcessor()) { amr.DataLog(0) << "# time max_temperature mean_temp" << std::endl; @@ -211,7 +374,7 @@ Case 1: C++ with Datalog Output .. code-block:: bash :caption: Configuration section of model.x - + # Configuration EXE="main3d.gnu.ex" INPUTS="inputs" @@ -219,7 +382,7 @@ Case 1: C++ with Datalog Output POSTPROCESSOR="./postprocess_datalog.sh" The ``model.x`` script (based on PyTUQ workflow examples) handles: - + - Reading input parameters from file - Setting up AMReX inputs command line options with parameters - Running the executable @@ -234,13 +397,13 @@ Case 1: C++ with Datalog Output 5. Use the pytuq infrastructure for setting up the inputs, e.g. .. code-block:: bash :caption: Configuration for input PC coefficients - - ## (a) Given mean and standard deviation of each normal random parameter - echo "1 0.1 " > param_margpc.txt - echo "1 0.1" >> param_margpc.txt - echo ".01 0.0025" >> param_margpc.txt - PC_TYPE=HG # Hermite-Gaussian PC - INPC_ORDER=1 + + ## (a) Given mean and standard deviation of each normal random parameter + echo "1 0.1 " > param_margpc.txt + echo "1 0.1" >> param_margpc.txt + echo ".01 0.0025" >> param_margpc.txt + PC_TYPE=HG # Hermite-Gaussian PC + INPC_ORDER=1 Case 2: PyAMReX Direct Integration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -261,7 +424,7 @@ Case 2: PyAMReX Direct Integration #Define inherited class with __call__ and outnames and pnames methods class HeatEquationModel(AMReXBaseModel) - + # Direct usage with PyTUQ model = HeatEquationModel() # model now provides the required interface @@ -306,6 +469,6 @@ Additional Resources .. seealso:: For complete working examples of the ``outputs = model(inputs)`` pattern, see: - + - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-1/`` - C++ wrappers - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-2/`` - PyAMReX native diff --git a/Docs/source/conf.py b/Docs/source/conf.py index a8f544f5..f8519ad7 100644 --- a/Docs/source/conf.py +++ b/Docs/source/conf.py @@ -39,6 +39,8 @@ def get_amrex_version(): extensions = ['sphinx.ext.mathjax', 'sphinx.ext.githubpages', 'sphinx.ext.viewcode', + 'sphinx_design', + 'sphinx_copybutton', 'sphinx.ext.intersphinx', 'sphinx_rtd_theme'] @@ -51,6 +53,9 @@ def get_amrex_version(): # Add any paths that contain templates here, relative to this directory. templates_path = ['ytemplates'] +# sphinx-copybutton configuration +copybutton_exclude = '.linenos, .gp, .go' + # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # From 8d638f34e6ae40ca044e2aae962768fd69f66250 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Tue, 7 Oct 2025 15:48:21 -0700 Subject: [PATCH 087/142] directory name --- Docs/source/HeatEquation_UQ.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 8ed2912e..41fdb96c 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -69,6 +69,11 @@ PyAMReX + PyTUQ **Prerequisites**: pyAMReX installed + .. code-block:: bash + :caption: Use Case-2 directory + + cd GuidedTutorials/HeatEquation_UQ/Case-2 + .. code-block:: bash :caption: Direct Python integration From b2903550a36f34be4908fea5f19365ec150447b2 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 05:16:26 -0700 Subject: [PATCH 088/142] Rename and move --- .../HeatEquation_UQ/Case-2/AMReXBaseModel.py | 131 +++++++++++++----- .../{Case-1 => Case-2}/CMakeLists.txt | 0 .../{Case-1 => Case-2}/ModelXBaseModel.py | 0 .../{Case-1 => Case-2}/main.cpp | 0 .../{Case-1 => Case-2}/model.x | 0 .../{Case-1 => Case-2}/pnames.txt | 0 .../{Case-1 => Case-2}/postprocess_datalog.sh | 0 .../Case-2}/AMReXBaseModel.py | 131 +++++------------- .../{Case-1 => Case-3/Case-2}/GNUmakefile | 0 .../{ => Case-3}/Case-2/HeatEquationModel.py | 0 .../{Case-1 => Case-3/Case-2}/Make.package | 0 .../{Case-1 => Case-3/Case-2}/inputs | 0 .../{ => Case-3}/Case-2/main.py | 0 13 files changed, 131 insertions(+), 131 deletions(-) rename GuidedTutorials/HeatEquation_UQ/{Case-1 => Case-2}/CMakeLists.txt (100%) rename GuidedTutorials/HeatEquation_UQ/{Case-1 => Case-2}/ModelXBaseModel.py (100%) rename GuidedTutorials/HeatEquation_UQ/{Case-1 => Case-2}/main.cpp (100%) rename GuidedTutorials/HeatEquation_UQ/{Case-1 => Case-2}/model.x (100%) rename GuidedTutorials/HeatEquation_UQ/{Case-1 => Case-2}/pnames.txt (100%) rename GuidedTutorials/HeatEquation_UQ/{Case-1 => Case-2}/postprocess_datalog.sh (100%) rename GuidedTutorials/HeatEquation_UQ/{Case-1 => Case-3/Case-2}/AMReXBaseModel.py (73%) rename GuidedTutorials/HeatEquation_UQ/{Case-1 => Case-3/Case-2}/GNUmakefile (100%) rename GuidedTutorials/HeatEquation_UQ/{ => Case-3}/Case-2/HeatEquationModel.py (100%) rename GuidedTutorials/HeatEquation_UQ/{Case-1 => Case-3/Case-2}/Make.package (100%) rename GuidedTutorials/HeatEquation_UQ/{Case-1 => Case-3/Case-2}/inputs (100%) rename GuidedTutorials/HeatEquation_UQ/{ => Case-3}/Case-2/main.py (100%) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py index 0cd3fdd9..a5c20bda 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py @@ -1,30 +1,32 @@ from pytuq.func.func import ModelWrapperFcn import numpy as np from abc import abstractmethod +import os +import tempfile from pytuq.func.func import ModelWrapperFcn import numpy as np -import amrex.space3d as amr - -def load_cupy(): - """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" - if amr.Config.have_gpu: - try: - import cupy as cp - amr.Print("Note: found and will use cupy") - return cp - except ImportError: - amr.Print("Warning: GPU found but cupy not available! Using numpy...") - import numpy as np - return np - if amr.Config.gpu_backend == "SYCL": - amr.Print("Warning: SYCL GPU backend not yet implemented for Python") - import numpy as np - return np - else: - import numpy as np - amr.Print("Note: found and will use numpy") - return np +#import amrex.space3d as amr + +#def load_cupy(): +# """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" +# if amr.Config.have_gpu: +# try: +# import cupy as cp +# amr.Print("Note: found and will use cupy") +# return cp +# except ImportError: +# amr.Print("Warning: GPU found but cupy not available! Using numpy...") +# import numpy as np +# return np +# if amr.Config.gpu_backend == "SYCL": +# amr.Print("Warning: SYCL GPU backend not yet implemented for Python") +# import numpy as np +# return np +# else: +# import numpy as np +# amr.Print("Note: found and will use numpy") +# return np class AMReXBaseModel(ModelWrapperFcn): """Base class for AMReX models with yt-style field info""" @@ -37,11 +39,20 @@ class AMReXBaseModel(ModelWrapperFcn): _request_out_fields = None _spatial_domain_bounds = None - def __init__(self, model=None, **kwargs): + # Subprocess configuration + _model_script = './model.x' # Path to model.x wrapper script + _use_subprocess = False # Enable subprocess mode + + def __init__(self, model=None, use_subprocess=False, model_script=None, **kwargs): # Initialize AMReX if needed - self.xp = load_cupy() - if not amr.initialized(): - amr.initialize([]) +# self.xp = load_cupy() +# if not amr.initialized(): +# amr.initialize([]) + + # Configure subprocess mode + self._use_subprocess = use_subprocess or self.__class__._use_subprocess + if model_script: + self._model_script = model_script # Create modelpar from existing parameter information modelpar = self._create_modelpar() @@ -164,7 +175,7 @@ def __call__(self, x): def _run_simulation(self, params): """ - Core simulation logic - override in subclasses or use evolve/postprocess. + Core simulation logic - uses subprocess if enabled. Args: params: numpy array of parameters (1D or 2D) @@ -178,20 +189,22 @@ def _run_simulation(self, params): n_samples = params.shape[0] outdim = getattr(self, 'outdim', len(getattr(self, 'output_names', [])) or 1) - outputs = np.zeros((n_samples, outdim)) - - # Check if subclass has evolve/postprocess methods - if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): - for i in range(n_samples): - # Evolve just returns simulation state - sim_state = self.evolve(params[i, :]) - # Postprocess extracts outputs from state - outputs[i, :] = self.postprocess(sim_state) + + if self._use_subprocess: + # Use model.x subprocess approach + outputs = self._run_subprocess(params) else: - raise NotImplementedError( - "Must implement _run_simulation or evolve/postprocess methods" - ) + # Original in-process method + outputs = np.zeros((n_samples, outdim)) + if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): + for i in range(n_samples): + sim_state = self.evolve(params[i, :]) + outputs[i, :] = self.postprocess(sim_state) + else: + raise NotImplementedError( + "Must implement _run_simulation or evolve/postprocess methods" + ) # Return only requested outputs if specified requested = self._request_out_fields or self._output_fields if requested != self._output_fields: @@ -202,6 +215,48 @@ def _run_simulation(self, params): return outputs + def _run_subprocess(self, params): + """ + Run simulation using model.x subprocess (like online_bb example). + + Args: + params: 2D numpy array (n_samples x n_params) + + Returns: + 2D numpy array (n_samples x n_outputs) + """ + # Check if model.x exists + if not os.path.exists(self._model_script): + raise FileNotFoundError(f"Model script not found: {self._model_script}") + + # Create temporary input/output files + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + input_file = f.name + np.savetxt(f, params) + + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + output_file = f.name + + try: + # Run model.x + cmd = f'{self._model_script} {input_file} {output_file}' + print(f"Running: {cmd}") + exit_code = os.system(cmd) + + if exit_code != 0: + raise RuntimeError(f"Command failed with exit code {exit_code}: {cmd}") + + # Load outputs + outputs = np.loadtxt(output_file).reshape(params.shape[0], -1) + + return outputs + + finally: + # Clean up temporary files + for f in [input_file, output_file]: + if os.path.exists(f): + os.unlink(f) + @property def field_list(self): """All available fields""" diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/CMakeLists.txt b/GuidedTutorials/HeatEquation_UQ/Case-2/CMakeLists.txt similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/CMakeLists.txt rename to GuidedTutorials/HeatEquation_UQ/Case-2/CMakeLists.txt diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/ModelXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-2/ModelXBaseModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/ModelXBaseModel.py rename to GuidedTutorials/HeatEquation_UQ/Case-2/ModelXBaseModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp b/GuidedTutorials/HeatEquation_UQ/Case-2/main.cpp similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp rename to GuidedTutorials/HeatEquation_UQ/Case-2/main.cpp diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/model.x b/GuidedTutorials/HeatEquation_UQ/Case-2/model.x similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/model.x rename to GuidedTutorials/HeatEquation_UQ/Case-2/model.x diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/pnames.txt b/GuidedTutorials/HeatEquation_UQ/Case-2/pnames.txt similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/pnames.txt rename to GuidedTutorials/HeatEquation_UQ/Case-2/pnames.txt diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/postprocess_datalog.sh b/GuidedTutorials/HeatEquation_UQ/Case-2/postprocess_datalog.sh similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/postprocess_datalog.sh rename to GuidedTutorials/HeatEquation_UQ/Case-2/postprocess_datalog.sh diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/AMReXBaseModel.py similarity index 73% rename from GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py rename to GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/AMReXBaseModel.py index a5c20bda..0cd3fdd9 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/AMReXBaseModel.py +++ b/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/AMReXBaseModel.py @@ -1,32 +1,30 @@ from pytuq.func.func import ModelWrapperFcn import numpy as np from abc import abstractmethod -import os -import tempfile from pytuq.func.func import ModelWrapperFcn import numpy as np -#import amrex.space3d as amr - -#def load_cupy(): -# """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" -# if amr.Config.have_gpu: -# try: -# import cupy as cp -# amr.Print("Note: found and will use cupy") -# return cp -# except ImportError: -# amr.Print("Warning: GPU found but cupy not available! Using numpy...") -# import numpy as np -# return np -# if amr.Config.gpu_backend == "SYCL": -# amr.Print("Warning: SYCL GPU backend not yet implemented for Python") -# import numpy as np -# return np -# else: -# import numpy as np -# amr.Print("Note: found and will use numpy") -# return np +import amrex.space3d as amr + +def load_cupy(): + """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" + if amr.Config.have_gpu: + try: + import cupy as cp + amr.Print("Note: found and will use cupy") + return cp + except ImportError: + amr.Print("Warning: GPU found but cupy not available! Using numpy...") + import numpy as np + return np + if amr.Config.gpu_backend == "SYCL": + amr.Print("Warning: SYCL GPU backend not yet implemented for Python") + import numpy as np + return np + else: + import numpy as np + amr.Print("Note: found and will use numpy") + return np class AMReXBaseModel(ModelWrapperFcn): """Base class for AMReX models with yt-style field info""" @@ -39,20 +37,11 @@ class AMReXBaseModel(ModelWrapperFcn): _request_out_fields = None _spatial_domain_bounds = None - # Subprocess configuration - _model_script = './model.x' # Path to model.x wrapper script - _use_subprocess = False # Enable subprocess mode - - def __init__(self, model=None, use_subprocess=False, model_script=None, **kwargs): + def __init__(self, model=None, **kwargs): # Initialize AMReX if needed -# self.xp = load_cupy() -# if not amr.initialized(): -# amr.initialize([]) - - # Configure subprocess mode - self._use_subprocess = use_subprocess or self.__class__._use_subprocess - if model_script: - self._model_script = model_script + self.xp = load_cupy() + if not amr.initialized(): + amr.initialize([]) # Create modelpar from existing parameter information modelpar = self._create_modelpar() @@ -175,7 +164,7 @@ def __call__(self, x): def _run_simulation(self, params): """ - Core simulation logic - uses subprocess if enabled. + Core simulation logic - override in subclasses or use evolve/postprocess. Args: params: numpy array of parameters (1D or 2D) @@ -189,22 +178,20 @@ def _run_simulation(self, params): n_samples = params.shape[0] outdim = getattr(self, 'outdim', len(getattr(self, 'output_names', [])) or 1) - - if self._use_subprocess: - # Use model.x subprocess approach - outputs = self._run_subprocess(params) + outputs = np.zeros((n_samples, outdim)) + + # Check if subclass has evolve/postprocess methods + if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): + for i in range(n_samples): + # Evolve just returns simulation state + sim_state = self.evolve(params[i, :]) + # Postprocess extracts outputs from state + outputs[i, :] = self.postprocess(sim_state) else: - # Original in-process method - outputs = np.zeros((n_samples, outdim)) + raise NotImplementedError( + "Must implement _run_simulation or evolve/postprocess methods" + ) - if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): - for i in range(n_samples): - sim_state = self.evolve(params[i, :]) - outputs[i, :] = self.postprocess(sim_state) - else: - raise NotImplementedError( - "Must implement _run_simulation or evolve/postprocess methods" - ) # Return only requested outputs if specified requested = self._request_out_fields or self._output_fields if requested != self._output_fields: @@ -215,48 +202,6 @@ def _run_simulation(self, params): return outputs - def _run_subprocess(self, params): - """ - Run simulation using model.x subprocess (like online_bb example). - - Args: - params: 2D numpy array (n_samples x n_params) - - Returns: - 2D numpy array (n_samples x n_outputs) - """ - # Check if model.x exists - if not os.path.exists(self._model_script): - raise FileNotFoundError(f"Model script not found: {self._model_script}") - - # Create temporary input/output files - with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: - input_file = f.name - np.savetxt(f, params) - - with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: - output_file = f.name - - try: - # Run model.x - cmd = f'{self._model_script} {input_file} {output_file}' - print(f"Running: {cmd}") - exit_code = os.system(cmd) - - if exit_code != 0: - raise RuntimeError(f"Command failed with exit code {exit_code}: {cmd}") - - # Load outputs - outputs = np.loadtxt(output_file).reshape(params.shape[0], -1) - - return outputs - - finally: - # Clean up temporary files - for f in [input_file, output_file]: - if os.path.exists(f): - os.unlink(f) - @property def field_list(self): """All available fields""" diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/GNUmakefile b/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/GNUmakefile similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/GNUmakefile rename to GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/GNUmakefile diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/HeatEquationModel.py b/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/HeatEquationModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/HeatEquationModel.py rename to GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/HeatEquationModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/Make.package b/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/Make.package similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/Make.package rename to GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/Make.package diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/inputs b/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/inputs similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/inputs rename to GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/inputs diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/main.py b/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/main.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/main.py rename to GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/main.py From e974f2be5f046414c30ceed0f4ffe3d1b5b0d794 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 05:17:55 -0700 Subject: [PATCH 089/142] Basic C++ example for workflow --- .../HeatEquation_UQ/Case-1/GNUmakefile | 16 + .../HeatEquation_UQ/Case-1/Make.package | 2 + GuidedTutorials/HeatEquation_UQ/Case-1/inputs | 16 + .../HeatEquation_UQ/Case-1/main.cpp | 324 ++++++++++++++++++ 4 files changed, 358 insertions(+) create mode 100644 GuidedTutorials/HeatEquation_UQ/Case-1/GNUmakefile create mode 100644 GuidedTutorials/HeatEquation_UQ/Case-1/Make.package create mode 100644 GuidedTutorials/HeatEquation_UQ/Case-1/inputs create mode 100644 GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/GNUmakefile b/GuidedTutorials/HeatEquation_UQ/Case-1/GNUmakefile new file mode 100644 index 00000000..f918d4f2 --- /dev/null +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/GNUmakefile @@ -0,0 +1,16 @@ +# AMREX_HOME defines the directory in which we will find all the AMReX code. +AMREX_HOME ?= ../../../../amrex + +DEBUG = FALSE +USE_MPI = FALSE +USE_OMP = FALSE +COMP = gnu +DIM = 3 + +include $(AMREX_HOME)/Tools/GNUMake/Make.defs + +include ./Make.package + +include $(AMREX_HOME)/Src/Base/Make.package + +include $(AMREX_HOME)/Tools/GNUMake/Make.rules diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/Make.package b/GuidedTutorials/HeatEquation_UQ/Case-1/Make.package new file mode 100644 index 00000000..7f43e5e8 --- /dev/null +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/Make.package @@ -0,0 +1,2 @@ +CEXE_sources += main.cpp + diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/inputs b/GuidedTutorials/HeatEquation_UQ/Case-1/inputs new file mode 100644 index 00000000..56ec738e --- /dev/null +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/inputs @@ -0,0 +1,16 @@ +# Grid/Domain parameters +n_cell = 32 # number of cells on each side of domain +max_grid_size = 16 # size of each box (or grid) + +# Time stepping parameters +nsteps = 100 # total steps in simulation +dt = 1.e-5 # time step + +# Output control +plot_int = 100 # how often to write plotfile (-1 = no plots) +datalog_int = -1 # how often to write datalog (-1 = no regular output) + +# Physics parameters (these are what we vary for UQ) +diffusion_coeff = 1.0 # diffusion coefficient for heat equation +init_amplitude = 1.0 # amplitude of initial temperature profile +init_width = 0.01 # width parameter (variance, not std dev) \ No newline at end of file diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp new file mode 100644 index 00000000..cf0ece87 --- /dev/null +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp @@ -0,0 +1,324 @@ +/* + * A simplified single file version of the HeatEquation_EX0_C exmaple. + * This code is designed to be used with Demo_Tutorial.rst. + * + */ + + +#include +#include +#include + +int main (int argc, char* argv[]) +{ + amrex::Initialize(argc,argv); + { + + // ********************************** + // DECLARE SIMULATION PARAMETERS + // ********************************** + + // number of cells on each side of the domain + int n_cell; + + // size of each box (or grid) + int max_grid_size; + + // total steps in simulation + int nsteps; + + // how often to write a plotfile + int plot_int; + + // time step + amrex::Real dt; + + // ********************************** + // DECLARE PHYSICS PARAMETERS + // ********************************** + + // diffusion coefficient for heat equation + amrex::Real diffusion_coeff; + + // amplitude of initial temperature profile + amrex::Real init_amplitude; + + // width parameter controlling spread of initial profile (variance, not std dev) + amrex::Real init_width; + + // ********************************** + // DECLARE DATALOG PARAMETERS + // ********************************** + const int datwidth = 24; + const int datprecision = 16; + const int timeprecision = 13; + int datalog_int = -1; // Interval for regular output (<=0 means no regular output) + bool datalog_final = true; // Write datalog at final step + + // ********************************** + // READ PARAMETER VALUES FROM INPUT DATA + // ********************************** + // inputs parameters + { + // ParmParse is way of reading inputs from the inputs file + // pp.get means we require the inputs file to have it + // pp.query means we optionally need the inputs file to have it - but we must supply a default here + amrex::ParmParse pp; + + // We need to get n_cell from the inputs file - this is the number of cells on each side of + // a square (or cubic) domain. + pp.get("n_cell",n_cell); + + // The domain is broken into boxes of size max_grid_size + pp.get("max_grid_size",max_grid_size); + + // Default nsteps to 10, allow us to set it to something else in the inputs file + nsteps = 10; + pp.query("nsteps",nsteps); + + // Default plot_int to -1, allow us to set it to something else in the inputs file + // If plot_int < 0 then no plot files will be written + plot_int = -1; + pp.query("plot_int",plot_int); + + // Default datalog_int to -1, allow us to set it to something else in the inputs file + // If datalog_int < 0 then no plot files will be written + datalog_int = -1; + pp.query("datalog_int",datalog_int); + + // time step + pp.get("dt",dt); + + // ********************************** + // READ PHYSICS PARAMETERS + // ********************************** + + // Diffusion coefficient - controls how fast heat spreads + diffusion_coeff = 1.0; + pp.query("diffusion_coeff", diffusion_coeff); // Note: input name is "diffusion" but variable is "diffusion_coeff" + + // Initial temperature amplitude + init_amplitude = 1.0; + pp.query("init_amplitude", init_amplitude); + + // Width parameter - this is the variance (width²), not standard deviation + // Smaller values = more concentrated, larger values = more spread out + init_width = 0.01; // Note: 0.01 to match your original rsquared/0.01 + pp.query("init_width", init_width); + } + + // ********************************** + // DEFINE SIMULATION SETUP AND GEOMETRY + // ********************************** + + // make BoxArray and Geometry + // ba will contain a list of boxes that cover the domain + // geom contains information such as the physical domain size, + // number of points in the domain, and periodicity + amrex::BoxArray ba; + amrex::Geometry geom; + + // define lower and upper indices + amrex::IntVect dom_lo(0,0,0); + amrex::IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); + + // Make a single box that is the entire domain + amrex::Box domain(dom_lo, dom_hi); + + // Initialize the boxarray "ba" from the single box "domain" + ba.define(domain); + + // Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction + ba.maxSize(max_grid_size); + + // This defines the physical box, [0,1] in each direction. + amrex::RealBox real_box({ 0., 0., 0.}, + { 1., 1., 1.}); + + // periodic in all direction + amrex::Array is_periodic{1,1,1}; + + // This defines a Geometry object + geom.define(domain, real_box, amrex::CoordSys::cartesian, is_periodic); + + // extract dx from the geometry object + amrex::GpuArray dx = geom.CellSizeArray(); + + // Nghost = number of ghost cells for each array + int Nghost = 1; + + // Ncomp = number of components for each array + int Ncomp = 1; + + // How Boxes are distrubuted among MPI processes + amrex::DistributionMapping dm(ba); + + // we allocate two phi multifabs; one will store the old state, the other the new. + amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); + amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); + + // time = starting time in the simulation + amrex::Real time = 0.0; + + // ********************************** + // INITIALIZE DATA LOOP + // ********************************** + + // loop over boxes + for (amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi) + { + const amrex::Box& bx = mfi.validbox(); + + const amrex::Array4& phiOld = phi_old.array(mfi); + + // ********************************** + // SET INITIAL TEMPERATURE PROFILE + // ********************************** + // Formula: T = 1 + amplitude * exp(-r^2 / width^2) + // where r is distance from center (0.5, 0.5, 0.5) + // + // Parameters: + // - amplitude: controls peak temperature above baseline (1.0) + // - width: controls spread of initial hot spot + // - smaller width = more concentrated + // - larger width = more spread out + +amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) + { + // Calculate physical coordinates of cell center + amrex::Real x = (i+0.5) * dx[0]; + amrex::Real y = (j+0.5) * dx[1]; + amrex::Real z = (k+0.5) * dx[2]; + + // Calculate squared distance from domain center (0.5, 0.5, 0.5) + // Divide by init_width (which is the variance, not standard deviation) + amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; + + // Set initial temperature profile + phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); + }); + } + + // ********************************** + // WRITE DATALOG FILE + // ********************************** + if (amrex::ParallelDescriptor::IOProcessor() && (datalog_int>0 || datalog_final)) { + std::ofstream datalog("datalog.txt"); // truncate mode to start fresh + datalog << "#" << std::setw(datwidth-1) << " max_temp"; + datalog << std::setw(datwidth) << " mean_temp"; + datalog << std::setw(datwidth) << " std_temp"; + datalog << std::setw(datwidth) << " total_energy"; + datalog << std::endl; + datalog.close(); + } + + // ********************************** + // WRITE INITIAL PLOT FILE + // ********************************** + + // Write a plotfile of the initial data if plot_int > 0 + if (plot_int > 0) + { + int step = 0; + const std::string& pltfile = amrex::Concatenate("plt",step,5); + WriteSingleLevelPlotfile(pltfile, phi_old, {"phi"}, geom, time, 0); + } + + + // ********************************** + // MAIN TIME EVOLUTION LOOP + // ********************************** + + for (int step = 1; step <= nsteps; ++step) + { + // fill periodic ghost cells + phi_old.FillBoundary(geom.periodicity()); + + // new_phi = old_phi + dt * Laplacian(old_phi) + // loop over boxes + for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ) + { + const amrex::Box& bx = mfi.validbox(); + + const amrex::Array4& phiOld = phi_old.array(mfi); + const amrex::Array4& phiNew = phi_new.array(mfi); + + // advance the data by dt using heat equation with diffusion coefficient + amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) + { + // Calculate the discrete Laplacian using finite differences + amrex::Real laplacian = + (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) + +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) + +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]); + + // Apply heat equation using diffusion_coeff - matches Python version + phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; + }); + } + + // ********************************** + // INCREMENT + // ********************************** + + // update time + time = time + dt; + + // copy new solution into old solution + amrex::MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); + + // Tell the I/O Processor to write out which step we're doing + amrex::Print() << "Advanced step " << step << "\n"; + + // ********************************** + // WRITE DATALOG AT GIVEN INTERVAL + // ********************************** + + // Check if we should write datalog + bool write_datalog = false; + if (datalog_final && step == nsteps) { + write_datalog = true; // Write final step + } else if (datalog_int > 0 && step % datalog_int == 0) { + write_datalog = true; // Write every datalog_int steps + } + + if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { + std::ofstream datalog("datalog.txt", std::ios::app); + + // Calculate temperature statistics + amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); + amrex::Real max_temperature = phi_new.max(0); + amrex::Real l2_norm = phi_new.norm2(0); + amrex::Real sum_sq = l2_norm * l2_norm; + amrex::Real variance = sum_sq / phi_new.boxArray().numPts() - mean_temp * mean_temp; + amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; + amrex::Real total_energy = phi_new.sum(0); + + // Write 4 statistics + datalog << std::setw(datwidth) << std::setprecision(datprecision) << max_temperature; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << mean_temp; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << std_temperature; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << total_energy; + datalog << std::endl; + + datalog.close(); + } + + // ********************************** + // WRITE PLOTFILE AT GIVEN INTERVAL + // ********************************** + + // Write a plotfile of the current data (plot_int was defined in the inputs file) + if (plot_int > 0 && step%plot_int == 0) + { + const std::string& pltfile = amrex::Concatenate("plt",step,5); + WriteSingleLevelPlotfile(pltfile, phi_new, {"phi"}, geom, time, step); + } + } + + } + amrex::Finalize(); + return 0; +} + + From 99c187602fd514673e5451ee7e02441a986ff970 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 05:31:09 -0700 Subject: [PATCH 090/142] Add modified workflow file and datalog filename input --- .../HeatEquation_UQ/Case-1/main.cpp | 5 ++- .../HeatEquation_UQ/Case-1/wf_uqpc.x | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100755 GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp index cf0ece87..1b6f4d0a 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp @@ -86,6 +86,9 @@ int main (int argc, char* argv[]) datalog_int = -1; pp.query("datalog_int",datalog_int); + datalog_filename = "datalog.txt"; + pp.query("datalog",datalog_filename); + // time step pp.get("dt",dt); @@ -203,7 +206,7 @@ amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) // WRITE DATALOG FILE // ********************************** if (amrex::ParallelDescriptor::IOProcessor() && (datalog_int>0 || datalog_final)) { - std::ofstream datalog("datalog.txt"); // truncate mode to start fresh + std::ofstream datalog(datalog_filename); // truncate mode to start fresh datalog << "#" << std::setw(datwidth-1) << " max_temp"; datalog << std::setw(datwidth) << " mean_temp"; datalog << std::setw(datwidth) << " std_temp"; diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x new file mode 100755 index 00000000..fb02c2fc --- /dev/null +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x @@ -0,0 +1,34 @@ +#!/bin/bash -e + +#SDIR=`dirname "$0"` +KLPC = $(pwd)/../../../../ + +#First-order Polynomial Chaos (PC). +## Given mean and standard deviation of each normal random parameter +echo "1 0.25 " > param_margpc.txt +echo "1 0.25" >> param_margpc.txt +echo "0.01 0.0025" >> param_margpc.txt + +echo "diffusion_coeff" > pnames.txt +echo "init_amplitude" >> pnames.txt +echo "init_width" >> pnames.txt + +echo "max_temp" > outnames.txt +echo "mean_temp" >> outnames.txt +echo "std_temp" >> outnames.txt +echo "total_energy" >> outnames.txt + +PCTYPE="HG" +ORDER=3 + +${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER +${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE + +parallel --jobs 1 --colsep ' ' \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + datalog=datalog_{#}.txt \ + plot_int = -1 \ + && tail -1 datalog_{#}.txt' \ + :::: qsam.txt > ysam.txt + +${KLPC}/apps/pc_fit.py $PCTYPE $ORDER From 523304030273e62ee68a4f1eba8eda16df4cf8a8 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 05:49:13 -0700 Subject: [PATCH 091/142] Fix paths, make pc fit use arguments --- GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp | 3 ++- GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp index 1b6f4d0a..49786599 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp @@ -54,6 +54,7 @@ int main (int argc, char* argv[]) const int timeprecision = 13; int datalog_int = -1; // Interval for regular output (<=0 means no regular output) bool datalog_final = true; // Write datalog at final step + std::string datalog_filename = "datalog.txt"; // ********************************** // READ PARAMETER VALUES FROM INPUT DATA @@ -286,7 +287,7 @@ amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) } if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { - std::ofstream datalog("datalog.txt", std::ios::app); + std::ofstream datalog(datalog_filename, std::ios::app); // Calculate temperature statistics amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x index fb02c2fc..39ececa2 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x @@ -1,7 +1,7 @@ #!/bin/bash -e #SDIR=`dirname "$0"` -KLPC = $(pwd)/../../../../ +export KLPC=$(pwd)/../../../../pytuq #First-order Polynomial Chaos (PC). ## Given mean and standard deviation of each normal random parameter @@ -19,16 +19,18 @@ echo "std_temp" >> outnames.txt echo "total_energy" >> outnames.txt PCTYPE="HG" -ORDER=3 +ORDER=1 ${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER -${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE +${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE 5 + +#cp qsam.txt ptrain.txt parallel --jobs 1 --colsep ' ' \ './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ datalog=datalog_{#}.txt \ - plot_int = -1 \ + plot_int = -1 > /dev/null 2>&1 \ && tail -1 datalog_{#}.txt' \ :::: qsam.txt > ysam.txt -${KLPC}/apps/pc_fit.py $PCTYPE $ORDER +${KLPC}/apps/pc_fit.py --pctype $PCTYPE --order $ORDER --xdata "qsam.txt" --ydata "ysam.txt" From 6a94879910f440e5aff82d2e6ff2e51e58bce27b Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 05:55:21 -0700 Subject: [PATCH 092/142] Change number of samples --- GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x index 39ececa2..f65f810c 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x @@ -20,9 +20,10 @@ echo "total_energy" >> outnames.txt PCTYPE="HG" ORDER=1 +NSAM=15 ${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER -${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE 5 +${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM #cp qsam.txt ptrain.txt From 146bf79f3f1d872e7800586372872be4ed206677 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 07:13:42 -0700 Subject: [PATCH 093/142] Use keep order and multiple jobs --- .../HeatEquation_UQ/Case-1/wf_uqpc.x | 6 +-- .../HeatEquation_UQ/Case-1/wf_uqpc_steps.x | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) create mode 100755 GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc_steps.x diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x index f65f810c..b97cecd1 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x @@ -20,14 +20,12 @@ echo "total_energy" >> outnames.txt PCTYPE="HG" ORDER=1 -NSAM=15 +NSAM=111 ${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER ${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM -#cp qsam.txt ptrain.txt - -parallel --jobs 1 --colsep ' ' \ +parallel --jobs 6 --keep-order --colsep ' ' \ './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ datalog=datalog_{#}.txt \ plot_int = -1 > /dev/null 2>&1 \ diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc_steps.x b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc_steps.x new file mode 100755 index 00000000..7beffc19 --- /dev/null +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc_steps.x @@ -0,0 +1,51 @@ +#!/bin/bash -e + +#SDIR=`dirname "$0"` +export KLPC=$(pwd)/../../../../pytuq + +#First-order Polynomial Chaos (PC). +## Given mean and standard deviation of each normal random parameter +echo "1 0.25 " > param_margpc.txt +echo "1 0.25" >> param_margpc.txt +echo "0.01 0.0025" >> param_margpc.txt + +echo "diffusion_coeff" > pnames.txt +echo "init_amplitude" >> pnames.txt +echo "init_width" >> pnames.txt + +echo "max_temp" > outnames.txt +echo "mean_temp" >> outnames.txt +echo "std_temp" >> outnames.txt +echo "total_energy" >> outnames.txt + +PCTYPE="HG" +ORDER=1 +NSAM=111 + +${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER +#${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM + +DIM=3 +NTRN=$NSAM +NTST=0 + +./uq_pc.py -r offline_prep -c pcf.txt -x ${PCTYPE} -d $DIM -o ${ORDER} -m lsq -s rand -n $NTRN -v $NTST + +cp ptrain.txt qsam.txt +#cp qsam.txt ptrain.txt + +parallel --jobs 6 --keep-order --colsep ' ' \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + datalog=datalog_{#}.txt \ + plot_int = -1 > /dev/null 2>&1 \ + && tail -1 datalog_{#}.txt' \ + :::: qsam.txt > ysam.txt + +cp ysam.txt ytrain.txt + +#${KLPC}/apps/pc_fit.py --pctype $PCTYPE --order $ORDER --xdata "qsam.txt" --ydata "ysam.txt" +./uq_pc.py -r offline_post -c pcf.txt -x ${PCTYPE} -d $DIM -o ${ORDER} -m lsq -s rand -n $NTRN -v $NTST -t 5 + +./plot.py sens main + +./plot.py jsens From a217c08cd30527f156e4bdd311b7e80bdcc40959 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 07:28:33 -0700 Subject: [PATCH 094/142] Streamline doc --- Docs/source/HeatEquation_UQ.rst | 280 +++++--------------------------- 1 file changed, 44 insertions(+), 236 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 41fdb96c..a4767b1d 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -28,8 +28,8 @@ Installation Examples -------- -C++ AMReX + PyTUQ -~~~~~~~~~~~~~~~~~ +C++ AMReX + PyTUQ (BASH driven) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. admonition:: Build and Run :class: dropdown @@ -42,6 +42,25 @@ C++ AMReX + PyTUQ cd GuidedTutorials/HeatEquation_UQ/Case-1 make -j4 + .. code-block:: bash + :caption: Run with bash script + + ./wf_uqpc.x + +C++ AMReX + PyTUQ (python driven) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. admonition:: Build and Run + :class: dropdown + + **Prerequisites**: AMReX compiled with MPI support + + .. code-block:: bash + :caption: Build C++ example + + cd GuidedTutorials/HeatEquation_UQ/Case-2 + make -j4 + .. code-block:: bash :caption: Copy ex_pcgsa.py and use the black box model @@ -72,7 +91,7 @@ PyAMReX + PyTUQ .. code-block:: bash :caption: Use Case-2 directory - cd GuidedTutorials/HeatEquation_UQ/Case-2 + cd GuidedTutorials/HeatEquation_UQ/Case-3 .. code-block:: bash :caption: Direct Python integration @@ -109,18 +128,19 @@ C++ AMReX on Perlmutter .. code-block:: bash :caption: perlmutter_build.sh - module load PrgEnv-gnu cuda + module load PrgEnv-gnu cudatoolkit make USE_MPI=TRUE USE_CUDA=TRUE .. code-block:: bash :caption: Submit job - sbatch run_amrex_uq.slurm + sbatch wk_uqpc.slurm -PyAMReX on Perlmutter -~~~~~~~~~~~~~~~~~~~~~ +.. PyAMReX on Perlmutter +.. ~~~~~~~~~~~~~~~~~~~~~ -.. admonition:: Perlmutter Setup +.. + admonition:: Perlmutter Setup :class: dropdown .. code-block:: bash @@ -143,135 +163,8 @@ PyAMReX on Perlmutter `_ for details. -Summary -------- - -**Key Takeaways:** - -* PyTUQ can use models with ``outputs = model(inputs)`` interface -* C++ codes need wrapper scripts; Python codes integrate directly -* Best practices for Perlmutter requires environment modules and MPI configuration - -Additional Resources --------------------- - -- `PyTUQ Documentation `_ -- `PyTUQ Examples directory `_ -- `AMReX Documentation `_ -- `pyAMReX Documentation `_ -- :ref:`_guided_heat` - Base tutorial this builds upon - -.. seealso:: - - For complete working examples of the ``outputs = model(inputs)`` pattern, see: - - - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-1/`` - C++ wrappers - - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-2/`` - PyAMReX native - -PyTUQ Integration with AMReX Applications (First draft) -======================================================= - -.. admonition:: **Time to Complete**: 30-45 minutes - :class: note - - **Prerequisites**: - - Basic knowledge of AMReX build system - - Familiarity with Python/NumPy - - Understanding of uncertainty quantification concepts - - **What you will learn**: - - How to wrap AMReX simulations with PyTUQ's generic model interface - - Converting simulation codes to ``outputs = model(inputs)`` pattern - - Processing simulation outputs for UQ analysis - -Goals ------ - -This tutorial demonstrates how to integrate PyTUQ (Python Uncertainty Quantification Toolkit) with AMReX-based applications. - -You will learn to: - -1. Configure wrappers for AMReX simulations to interface to PyTUQ -2. Use inputs and extract datalog outputs -3. Choose the appropriate integration approach for your workflow -4. Run sensitivity analysis and inverse modeling using PyTUQ - -Prerequisites and Setup ------------------------ - -Required Dependencies -~~~~~~~~~~~~~~~~~~~~~ - -Install pytuq as described in `pytuq/README.md `_: - -.. code-block:: bash - :caption: Pytuq installation script - - #!/bin/bash - - # For NERSC: module load conda - - # 1. Clone repositories - git clone --recursive --branch v1.0.0z https://github.com/sandialabs/pytuq - - # 2. Setup conda environment (optional, you can add to an existing env) - # Create conda environment (use -y for non-interactive) - conda create -y --name pytuq_integration python=3.11 --no-default-packages - - # For NERSC (see https://docs.nersc.gov/development/languages/python/nersc-python/#moving-your-conda-setup-to-globalcommonsoftware): - # conda create -y --prefix /global/common/software/myproject/$USER/pytuq_integration python=3.11 - - conda activate pytuq_integration - # For NERSC: conda activate /global/common/software/myproject/$USER/pytuq_integration - - # 3. Install PyTUQ - cd pytuq - python -m pip install -r requirements.txt - python -m pip install . - conda install -y dill - cd ../ - - # 4. Verify installation - conda list | grep pytuq # Should show pytuq 1.0.0z - -For a full install including this tutorial, amrex, pyamrex, and pytuq see `example_detailed_install.sh <../../../GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh>`_ - -.. note:: - - For NERSC users, consider placing your conda environment in ``/global/common/software`` - for better performance and persistence. See the `NERSC Python documentation - `_ - for details. - -Reference PyTUQ Workflow Examples -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -PyTUQ provides several workflow examples demonstrating the model interface: - -.. list-table:: - :header-rows: 1 - - * - Analysis Type - - Example File - - Model Interface Pattern - * - Global Sensitivity - - `ex_pcgsa.py `_ - - ``myfunc = Ishigami(); ysam = myfunc(xsam)`` with polynomial chaos - * - Inverse Modeling - - `ex_mcmc_fitmodel.py `_ - - ``true_model, true_model_params = linear_model, {'W': W, 'b': b}; yd = true_model(true_model_input, true_model_params)`` for likelihood evaluation - * - Gaussian Process - - `ex_gp.py `_ - - Surrogate: ``true_model = sin4; y = true_model(x)+datastd*np.random.randn(ntrn)`` - * - Linear Regression - - `ex_lreg_merr.py `_ - - ``true_model = lambda x: x[:,0]**4 - 2.*x[:,0]**3 #fcb.sin4; y = true_model(x)`` - -Integration Cases ------------------ - -Case 1: C++ Application Wrappers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Extending this tutorial to other applications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Implementation Steps:** @@ -307,18 +200,10 @@ Case 1: C++ Application Wrappers // Evolution phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; -.. note:: - - The key to PyTUQ integration is creating a loop that maps input parameters to output quantities. - This loop structure differs between cases: - - - **Case 1**: Bash workflow manages run directories, runs executable with input parameters, parses outputs - - **Case 2**: Python loop directly calls pyAMReX functions - The simplest output extraction method is described here: -Case 1: C++ with Datalog Output -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +C++ with Datalog Output +~~~~~~~~~~~~~~~~~~~~~~~ .. admonition:: When to use :class: tip @@ -375,105 +260,28 @@ Case 1: C++ with Datalog Output amr.DataLog(0) << time << " " << max_temperature << " " << mean_temp << std::endl; } -3. Configure bash wrapper (``model.x``): - - .. code-block:: bash - :caption: Configuration section of model.x - - # Configuration - EXE="main3d.gnu.ex" - INPUTS="inputs" - INCLUDE_HEADER=true - POSTPROCESSOR="./postprocess_datalog.sh" - - The ``model.x`` script (based on PyTUQ workflow examples) handles: - - - Reading input parameters from file - - Setting up AMReX inputs command line options with parameters - - Running the executable - - Calling postprocessor to extract outputs - - Writing outputs to file - -4. Set up files like pnames.txt - diffusion_coeff - init_amplitude - init_width - -5. Use the pytuq infrastructure for setting up the inputs, e.g. - .. code-block:: bash - :caption: Configuration for input PC coefficients - - ## (a) Given mean and standard deviation of each normal random parameter - echo "1 0.1 " > param_margpc.txt - echo "1 0.1" >> param_margpc.txt - echo ".01 0.0025" >> param_margpc.txt - PC_TYPE=HG # Hermite-Gaussian PC - INPC_ORDER=1 - -Case 2: PyAMReX Direct Integration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. admonition:: When to use - :class: tip - - For Python-based AMReX applications - provides native model interface. - -**Implementation:** - -.. code-block:: python - :caption: HeatEquationModel.py - - import numpy as np - import pyamrex.amrex as amrex - from AMReXBaseModel import AMReXBaseModel - - #Define inherited class with __call__ and outnames and pnames methods - class HeatEquationModel(AMReXBaseModel) - - # Direct usage with PyTUQ - model = HeatEquationModel() - # model now provides the required interface - -Running Complete UQ Workflows ------------------------------- - -Example workflow combining model with PyTUQ analysis: -[add example from pytuq examples] - -Troubleshooting ---------------- - -Common Issues and Solutions -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Summary +------- -.. list-table:: - :header-rows: 1 +**Key Takeaways:** - * - Issue - - Solution - * - Model returns wrong shape - - Ensure output is ``[n_samples, n_outputs]``, use ``reshape(-1, n_outputs)`` - * - Model crashes on certain parameters - - Add parameter validation and bounds checking in wrapper - * - Slow execution for many samples - - Consider parallel execution or surrogate models - * - Outputs uncorrelated with inputs - - Verify parameters are actually being used in simulation - * - Memory issues with large runs - - Process in batches, clean up temporary files +* PyTUQ can use models with ``outputs = model(inputs)`` interface +* C++ codes can use wrapper scripts or gnu parallel; Python codes integrate directly +* Best practices for Perlmutter requires python environment modules Additional Resources -------------------- -- `PyTUQ Documentation `_ -- `PyTUQ Examples Repository `_ +- `PyTUQ Documentation `_ +- `PyTUQ Examples directory `_ - `AMReX Documentation `_ - `pyAMReX Documentation `_ -- :ref:`guided_heat_equation` - Base tutorial this builds upon +- :ref:`_guided_heat` - Base tutorial this builds upon .. seealso:: For complete working examples of the ``outputs = model(inputs)`` pattern, see: - - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-1/`` - C++ wrappers - - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-2/`` - PyAMReX native + - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-1/`` - C++ executable and python scripts called from a bash workflow script + - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-2/`` - C++ executable driven by python wrapping bash + - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-3/`` - PyAMReX native From d3b94531dde2328363fa155d8d23cdf4c140ab9b Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 07:43:30 -0700 Subject: [PATCH 095/142] Tweak heading levels --- Docs/source/HeatEquation_UQ.rst | 37 ++++++++++++++++++--------------- Docs/source/index.rst | 4 ++++ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index a4767b1d..43b07eb9 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -48,7 +48,7 @@ C++ AMReX + PyTUQ (BASH driven) ./wf_uqpc.x C++ AMReX + PyTUQ (python driven) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. admonition:: Build and Run :class: dropdown @@ -89,23 +89,23 @@ PyAMReX + PyTUQ **Prerequisites**: pyAMReX installed .. code-block:: bash - :caption: Use Case-2 directory + :caption: Use Case-2 directory - cd GuidedTutorials/HeatEquation_UQ/Case-3 + cd GuidedTutorials/HeatEquation_UQ/Case-3 - .. code-block:: bash - :caption: Direct Python integration + .. code-block:: bash + :caption: Direct Python integration - cp ../../../../pytuq/examples/ex_pcgsa.py - # replace Ishigami() with pyamrex model + cp ../../../../pytuq/examples/ex_pcgsa.py + # replace Ishigami() with pyamrex model - 16d15 - < from HeatEquationModel import HeatEquationModel - 23,24c22 - < myfunc = HeatEquationModel() - < myfunc._request_out_fields = [('output', 'max_temp')] - --- - > myfunc = Ishigami() + 16d15 + < from HeatEquationModel import HeatEquationModel + 23,24c22 + < myfunc = HeatEquationModel() + < myfunc._request_out_fields = [('output', 'max_temp')] + --- + > myfunc = Ishigami() .. code-block:: bash :caption: Run @@ -164,7 +164,10 @@ C++ AMReX on Perlmutter for details. Extending this tutorial to other applications -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +--------------------------------------------- + +Identify / Add input parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Implementation Steps:** @@ -202,8 +205,8 @@ Extending this tutorial to other applications The simplest output extraction method is described here: -C++ with Datalog Output -~~~~~~~~~~~~~~~~~~~~~~~ +Output example: C++ Datalog +~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. admonition:: When to use :class: tip diff --git a/Docs/source/index.rst b/Docs/source/index.rst index f185fc6a..5ed1d232 100644 --- a/Docs/source/index.rst +++ b/Docs/source/index.rst @@ -50,6 +50,7 @@ sorted by the following categories: - :ref:`GPU` -- Offload work to the GPUs using AMReX tools. - :ref:`Linear Solvers` -- Examples of several linear solvers. - :ref:`ML/PYTORCH` -- Use of pytorch models to replace point-wise computational kernels. +- :ref:`AMReX-pytuq` -- Use of pytuq methods to perform generalized sensitivity analysis. - :ref:`MPMD` -- Usage of AMReX-MPMD (Multiple Program Multiple Data) framework. - :ref:`MUI` -- Incorporates the MxUI/MUI (Multiscale Universal interface) frame into AMReX. - :ref:`Particles` -- Basic usage of AMReX's particle data structures. @@ -72,6 +73,7 @@ sorted by the following categories: GPU_Tutorial LinearSolvers_Tutorial ML_Tutorial + HeatEquation_UQ MPMD_Tutorials MUI_Tutorial Particles_Tutorial @@ -97,6 +99,8 @@ sorted by the following categories: .. _`Linear Solvers`: LinearSolvers_Tutorial.html +.. _`UQ`: UQ_Tutorial.html + .. _`MPMD`: MPMD_Tutorials.html .. _`MUI`: MUI_Tutorial.html From b8e035e44cca2e17ec09143b29965179fe649cc3 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 07:47:30 -0700 Subject: [PATCH 096/142] Move conda description for perlmutter --- Docs/source/HeatEquation_UQ.rst | 37 ++++++-------- .../HeatEquation_UQ/Case-1/wf_uqpc.slurm | 50 +++++++++++++++++++ 2 files changed, 66 insertions(+), 21 deletions(-) create mode 100755 GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 43b07eb9..27a750da 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -125,6 +125,19 @@ C++ AMReX on Perlmutter .. admonition:: Perlmutter Setup :class: dropdown + .. code-block:: bash + :caption: Virtual environment setup + + module load conda + # For NERSC (see https://docs.nersc.gov/development/languages/python/nersc-python/#moving-your-conda-setup-to-globalcommonsoftware): + # conda create -y --prefix /global/common/software/myproject/$USER/pytuq_integration python=3.11 + conda create -y --name pytuq_integration python=3.11 + git clone --recursive --branch v1.0.0z https://github.com/sandialabs/pytuq + cd pytuq + echo "dill" >> requirements.txt + pip install -r requirements.txt + pip install . + .. code-block:: bash :caption: perlmutter_build.sh @@ -134,31 +147,13 @@ C++ AMReX on Perlmutter .. code-block:: bash :caption: Submit job + # If stored in common software: conda activate /global/common/software/myproject/$USER/pytuq_integration + conda activate pytuq_integration sbatch wk_uqpc.slurm -.. PyAMReX on Perlmutter -.. ~~~~~~~~~~~~~~~~~~~~~ - -.. - admonition:: Perlmutter Setup - :class: dropdown - - .. code-block:: bash - :caption: Virtual environment setup - - module load conda - # For NERSC (see https://docs.nersc.gov/development/languages/python/nersc-python/#moving-your-conda-setup-to-globalcommonsoftware): - conda create -y --prefix /global/common/software/myproject/$USER/pytuq_integration python=3.11 - - - .. code-block:: bash - :caption: Run PyAMReX + PyTUQ - - srun -n 4 python run_pyamrex_uq.py - .. note:: - For NERSC users, consider placing your conda environment in ``/global/common/software`` + For NERSC, consider placing your conda environment in ``/global/common/software`` for better performance and persistence. See the `NERSC Python documentation `_ for details. diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm new file mode 100755 index 00000000..7c3bc146 --- /dev/null +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm @@ -0,0 +1,50 @@ +#!/bin/bash +#SBATCH --account=mp111_g +#SBATCH --nodes=1 +#SBATCH --ntasks-per-node=4 +#SBATCH -c 32 +#SBATCH --gpus-per-task=1 +#SBATCH --gpu-bind=none +#SBATCH --time=00:10:00 +#SBATCH --constraint=gpu&hbm40g +#SBATCH --qos=regular + +export MPICH_GPU_SUPPORT_ENABLED=1 +export SLURM_CPU_BIND="cores" + +# NOTE sbatch choices here are based on https://github.com/WeiqunZhang/amrex-scaling/blob/824551bc0189f374317bf8602bb81799deb970a2/fft/perlmutter/2025-02-06/run-16.sh +# NOTE this example is only meant for one node + +#SDIR=`dirname "$0"` +export KLPC=$(pwd)/../../../../pytuq + +#First-order Polynomial Chaos (PC). +## Given mean and standard deviation of each normal random parameter +echo "1 0.25 " > param_margpc.txt +echo "1 0.25" >> param_margpc.txt +echo "0.01 0.0025" >> param_margpc.txt + +echo "diffusion_coeff" > pnames.txt +echo "init_amplitude" >> pnames.txt +echo "init_width" >> pnames.txt + +echo "max_temp" > outnames.txt +echo "mean_temp" >> outnames.txt +echo "std_temp" >> outnames.txt +echo "total_energy" >> outnames.txt + +PCTYPE="HG" +ORDER=1 +NSAM=111 + +${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER +${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM + +srun parallel --jobs 4 --keep-order --colsep ' ' \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + datalog=datalog_{#}.txt \ + plot_int = -1 > /dev/null 2>&1 \ + && tail -1 datalog_{#}.txt' \ + :::: qsam.txt > ysam.txt + +${KLPC}/apps/pc_fit.py --pctype $PCTYPE --order $ORDER --xdata "qsam.txt" --ydata "ysam.txt" From 6245166dae8b853878e904bff9968a8a9b4e5881 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 08:01:54 -0700 Subject: [PATCH 097/142] Tweak srun vs parallel tasks --- GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm index 7c3bc146..10467d32 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm @@ -1,9 +1,9 @@ #!/bin/bash #SBATCH --account=mp111_g #SBATCH --nodes=1 -#SBATCH --ntasks-per-node=4 -#SBATCH -c 32 -#SBATCH --gpus-per-task=1 +#SBATCH --ntasks-per-node=1 +#SBATCH -c 128 +#SBATCH --gpus-per-task=4 #SBATCH --gpu-bind=none #SBATCH --time=00:10:00 #SBATCH --constraint=gpu&hbm40g @@ -12,7 +12,7 @@ export MPICH_GPU_SUPPORT_ENABLED=1 export SLURM_CPU_BIND="cores" -# NOTE sbatch choices here are based on https://github.com/WeiqunZhang/amrex-scaling/blob/824551bc0189f374317bf8602bb81799deb970a2/fft/perlmutter/2025-02-06/run-16.sh +# NOTE sbatch choices are altered here to make gnu parallel work with the ones in https://github.com/WeiqunZhang/amrex-scaling/blob/824551bc0189f374317bf8602bb81799deb970a2/fft/perlmutter/2025-02-06/run-16.sh # NOTE this example is only meant for one node #SDIR=`dirname "$0"` From 7d02b2158a6564c94fa567a34ca15cab833fe667 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 08:51:56 -0700 Subject: [PATCH 098/142] Add motivation and math details section --- Docs/source/HeatEquation_UQ.rst | 100 ++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 27a750da..98f81859 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -13,6 +13,19 @@ PyTUQ Quick Start Guide - Run AMReX + PyTUQ examples - Deploy on Perlmutter +Motivation +---------- + +AMReX simulations deliver high-fidelity, accurate results for complex physics problems, but parameter studies and uncertainty quantification can require hundreds or thousands of runs—making comprehensive analysis computationally prohibitive. + +This tutorial demonstrates how to improve efficiency without sacrificing accuracy by using polynomial chaos expansions to build fast surrogate models that identify which input parameters truly matter. + +PyTUQ (Python interface to the UQ Toolkit) provides specialized tools for surrogate construction and global sensitivity analysis, enabling rapid parameter space exploration and dimensionality reduction for scientific applications. + +We demonstrate how to integrate PyTUQ with your AMReX application through three practical workflows: C++ executables with file-based I/O (Case-1), Python-wrapped C++ codes (Case-2), and native PyAMReX applications (Case-3). + +Located in ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ``, this example analyzes a heat equation solver to illustrate the complete forward UQ workflow from parameter sampling through sensitivity analysis. + Installation ------------ @@ -158,6 +171,93 @@ C++ AMReX on Perlmutter `_ for details. +Mathematical Details +-------------------- + +Parameter Sensitivity via Polynomial Chaos +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this example, PyTUQ constructs polynomial chaos expansions to analyze how uncertain physical parameters affect simulation outputs in a heat diffusion problem. + +The Heat Equation +^^^^^^^^^^^^^^^^^ + +The governing equation for this tutorial is the heat diffusion equation: + +.. math:: + + \frac{\partial T}{\partial t} = D \nabla^2 T + S(x,y,z) + +where: + +- :math:`T` is the temperature field +- :math:`D` is the diffusion coefficient (``diffusion_coeff``) +- :math:`S(x,y,z)` is an optional source term (not used in this example) + +The initial temperature profile is a Gaussian centered at (0.5, 0.5, 0.5): + +.. math:: + + T(x,y,z,t=0) = 1 + A \exp\left(-\frac{r^2}{w^2}\right) + +where: + +- :math:`A` is the initial amplitude (``init_amplitude``) +- :math:`w^2` is the initial width parameter (``init_width``) +- :math:`r^2 = (x-0.5)^2 + (y-0.5)^2 + (z-0.5)^2` + +Uncertain Input Parameters +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The three uncertain parameters in this analysis are: + +1. **diffusion_coeff** (:math:`D`): Controls how fast heat spreads through the domain + + - Mean: 1.0 m²/s + - Standard deviation: 0.25 m²/s + - Range: [0.25, 1.75] m²/s + +2. **init_amplitude** (:math:`A`): Peak temperature above baseline + + - Mean: 1.0 K + - Standard deviation: 0.25 K + - Range: [0.25, 1.75] K + +3. **init_width** (:math:`w^2`): Controls spread of initial temperature profile + + - Mean: 0.01 m² + - Standard deviation: 0.0025 m² + - Range: [0.0025, 0.0175] m² + +These parameters are specified in the AMReX inputs file and read using ``ParmParse::query()`` (see ``main.cpp`` lines 100-111). + +Quantities of Interest (Outputs) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The simulation extracts four statistical quantities at the final timestep: + +1. **max_temp**: Maximum temperature in the domain +2. **mean_temp**: Average temperature across all cells +3. **std_temp**: Standard deviation of temperature +4. **total_energy**: Sum of temperature values (proportional to total thermal energy) + +These outputs are computed in ``main.cpp`` (lines 293-299) and written to the datalog file. + +PyTUQ Workflow +^^^^^^^^^^^^^^ + +PyTUQ uses polynomial chaos expansion to construct a surrogate model: + +1. **Parameter sampling**: Generate sample points in the 3D parameter space based on the specified distributions +2. **Forward simulations**: Run the AMReX heat equation solver for each parameter set +3. **Surrogate construction**: Fit polynomial chaos coefficients to map inputs → outputs +4. **Sensitivity analysis**: Compute Sobol indices to identify which parameters most influence each output + +The connection is: + +- **Inputs**: ParmParse parameters (``diffusion_coeff``, ``init_amplitude``, ``init_width``) specified in ``inputs`` file or command line +- **Outputs**: Quantities of interest extracted from datalog files or direct Python access to MultiFabs + Extending this tutorial to other applications --------------------------------------------- From aa82e92d39e97bdab7c9d24e7152d0ee7557e05f Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 09:01:40 -0700 Subject: [PATCH 099/142] Collapsible code --- Docs/source/HeatEquation_UQ.rst | 94 ++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 31 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 98f81859..9043dec0 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -44,8 +44,7 @@ Examples C++ AMReX + PyTUQ (BASH driven) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. admonition:: Build and Run - :class: dropdown +.. dropdown:: Build and Run **Prerequisites**: AMReX compiled with MPI support @@ -63,8 +62,7 @@ C++ AMReX + PyTUQ (BASH driven) C++ AMReX + PyTUQ (python driven) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. admonition:: Build and Run - :class: dropdown +.. dropdown:: Build and Run **Prerequisites**: AMReX compiled with MPI support @@ -74,54 +72,89 @@ C++ AMReX + PyTUQ (python driven) cd GuidedTutorials/HeatEquation_UQ/Case-2 make -j4 + .. note:: + + **Adapting the PyTUQ example script:** + + Copy the PyTUQ example script and replace the Ishigami test function with the HeatEquationModel wrapper. This allows PyTUQ to drive the C++ executable through Python subprocess calls. + .. code-block:: bash - :caption: Copy ex_pcgsa.py and use the black box model + :caption: Copy PyTUQ example script + + cp ../../../../pytuq/examples/ex_pcgsa.py . + + **Modify ex_pcgsa.py:** + + Add the import near the top of the file: + + .. code-block:: python + + from ModelXBaseModel import HeatEquationModel + + Replace the model definition: + + .. code-block:: python + :caption: Original (around line 23) + + myfunc = Ishigami() - cp ../../../../pytuq/examples/ex_pcgsa.py - # replace Ishigami() with minimal python model + .. code-block:: python + :caption: Replacement - 16d15 - < from ModelXBaseModel import HeatEquationModel - 23,24c22 - < myfunc = HeatEquationModel() - < myfunc._request_out_fields = [('output', 'max_temp')] - --- - > myfunc = Ishigami() + myfunc = HeatEquationModel() + myfunc._request_out_fields = [('output', 'max_temp')] .. code-block:: bash - :caption: Run with Python wrapped bash + :caption: Run UQ analysis python ./ex_pcgsa.py PyAMReX + PyTUQ ~~~~~~~~~~~~~~~ -.. admonition:: Setup and Run - :class: dropdown +.. dropdown:: Setup and Run **Prerequisites**: pyAMReX installed .. code-block:: bash - :caption: Use Case-2 directory + :caption: Navigate to Case-3 directory cd GuidedTutorials/HeatEquation_UQ/Case-3 + .. note:: + + **Using PyAMReX with PyTUQ:** + + For native PyAMReX applications, copy the PyTUQ example script and replace the Ishigami function with the PyAMReX-based HeatEquationModel. This enables direct Python-to-Python integration without subprocess overhead. + .. code-block:: bash - :caption: Direct Python integration + :caption: Copy PyTUQ example script + + cp ../../../../pytuq/examples/ex_pcgsa.py . + + **Modify ex_pcgsa.py:** + + Add the import near the top of the file: + + .. code-block:: python + + from HeatEquationModel import HeatEquationModel + + Replace the model definition: + + .. code-block:: python + :caption: Original (around line 23) + + myfunc = Ishigami() - cp ../../../../pytuq/examples/ex_pcgsa.py - # replace Ishigami() with pyamrex model + .. code-block:: python + :caption: Replacement - 16d15 - < from HeatEquationModel import HeatEquationModel - 23,24c22 - < myfunc = HeatEquationModel() - < myfunc._request_out_fields = [('output', 'max_temp')] - --- - > myfunc = Ishigami() + myfunc = HeatEquationModel() + myfunc._request_out_fields = [('output', 'max_temp')] .. code-block:: bash - :caption: Run + :caption: Run UQ analysis python ./ex_pcgsa.py @@ -135,8 +168,7 @@ Perlmutter Deployment (not implemented) C++ AMReX on Perlmutter ~~~~~~~~~~~~~~~~~~~~~~~~ -.. admonition:: Perlmutter Setup - :class: dropdown +.. dropdown:: Perlmutter Setup .. code-block:: bash :caption: Virtual environment setup From 05b900bd2387167b7b08ba2656585d698f2926de Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 09:08:13 -0700 Subject: [PATCH 100/142] Fix levels --- Docs/source/HeatEquation_UQ.rst | 8 ++++---- .../HeatEquation_UQ/Case-3/{Case-2 => }/AMReXBaseModel.py | 0 .../HeatEquation_UQ/Case-3/{Case-2 => }/GNUmakefile | 0 .../Case-3/{Case-2 => }/HeatEquationModel.py | 0 .../HeatEquation_UQ/Case-3/{Case-2 => }/Make.package | 0 .../HeatEquation_UQ/Case-3/{Case-2 => }/inputs | 0 .../HeatEquation_UQ/Case-3/{Case-2 => }/main.py | 0 7 files changed, 4 insertions(+), 4 deletions(-) rename GuidedTutorials/HeatEquation_UQ/Case-3/{Case-2 => }/AMReXBaseModel.py (100%) rename GuidedTutorials/HeatEquation_UQ/Case-3/{Case-2 => }/GNUmakefile (100%) rename GuidedTutorials/HeatEquation_UQ/Case-3/{Case-2 => }/HeatEquationModel.py (100%) rename GuidedTutorials/HeatEquation_UQ/Case-3/{Case-2 => }/Make.package (100%) rename GuidedTutorials/HeatEquation_UQ/Case-3/{Case-2 => }/inputs (100%) rename GuidedTutorials/HeatEquation_UQ/Case-3/{Case-2 => }/main.py (100%) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 9043dec0..a52db444 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -2,8 +2,8 @@ .. _pytuq_quickstart: -PyTUQ Quick Start Guide -======================= +PyTUQ +===== .. admonition:: **Time to Complete**: 15-20 minutes :class: note @@ -158,8 +158,8 @@ PyAMReX + PyTUQ python ./ex_pcgsa.py -Perlmutter Deployment (not implemented) ---------------------------------------- +Perlmutter Deployment +--------------------- .. note:: diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/AMReXBaseModel.py b/GuidedTutorials/HeatEquation_UQ/Case-3/AMReXBaseModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/AMReXBaseModel.py rename to GuidedTutorials/HeatEquation_UQ/Case-3/AMReXBaseModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/GNUmakefile b/GuidedTutorials/HeatEquation_UQ/Case-3/GNUmakefile similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/GNUmakefile rename to GuidedTutorials/HeatEquation_UQ/Case-3/GNUmakefile diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/HeatEquationModel.py b/GuidedTutorials/HeatEquation_UQ/Case-3/HeatEquationModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/HeatEquationModel.py rename to GuidedTutorials/HeatEquation_UQ/Case-3/HeatEquationModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/Make.package b/GuidedTutorials/HeatEquation_UQ/Case-3/Make.package similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/Make.package rename to GuidedTutorials/HeatEquation_UQ/Case-3/Make.package diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/inputs b/GuidedTutorials/HeatEquation_UQ/Case-3/inputs similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/inputs rename to GuidedTutorials/HeatEquation_UQ/Case-3/inputs diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/main.py b/GuidedTutorials/HeatEquation_UQ/Case-3/main.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/Case-2/main.py rename to GuidedTutorials/HeatEquation_UQ/Case-3/main.py From 3649dbaa11bdff3cd53a65025481f6aa4d933fd5 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 09:14:34 -0700 Subject: [PATCH 101/142] Add pyamrex notes --- Docs/source/HeatEquation_UQ.rst | 112 ++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index a52db444..4e15c9f1 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -390,6 +390,118 @@ Output example: C++ Datalog amr.DataLog(0) << time << " " << max_temperature << " " << mean_temp << std::endl; } +Extending to PyAMReX Applications +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For PyAMReX applications, adapt your existing ``main.py`` to enable UQ parameter sweeps: + +**1. Modify main() function signature** + +Add UQ parameters as function arguments: + +.. code-block:: python + :caption: Original + + def main(n_cell, max_grid_size, nsteps, plot_int, dt): + +.. code-block:: python + :caption: With UQ parameters + + def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, + plot_int: int = 100, dt: float = 1e-5, plot_files_output: bool = False, + verbose: int = 1, diffusion_coeff: float = 1.0, init_amplitude: float = 1.0, + init_width: float = 0.01) -> Tuple[amr.MultiFab, amr.Geometry]: + +**2. Use parameters in physics** + +Replace hardcoded values with function parameters: + +.. code-block:: python + :caption: Original initial condition (main.py:117-118) + + rsquared = ((x[:, xp.newaxis, xp.newaxis] - 0.5)**2 + + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 + + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / 0.01 + phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + xp.exp(-rsquared) + +.. code-block:: python + :caption: Parameterized + + rsquared = ((x[:, xp.newaxis, xp.newaxis] - 0.5)**2 + + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 + + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / init_width + phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + init_amplitude * xp.exp(-rsquared) + +.. code-block:: python + :caption: Original evolution (main.py:143-144) + + phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( + phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + + dt*((phiOld[:, ngz:-ngz, ngy:-ngy, ngx+1:hix-ngx+1] - ... ) / dx[0]**2)) + +.. code-block:: python + :caption: With diffusion coefficient + + phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( + phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + + dt * diffusion_coeff * ((phiOld[:, ngz:-ngz, ngy:-ngy, ngx+1:hix-ngx+1] - ... ) / dx[0]**2)) + +**3. Return simulation state** + +Modify the main function to return MultiFab and Geometry: + +.. code-block:: python + + return phi_new, geom + +**4. Create PyTUQ model wrapper** + +Create a ``HeatEquationModel.py`` that inherits from ``AMReXBaseModel``: + +.. code-block:: python + + from AMReXBaseModel import AMReXBaseModel + import amrex.space3d as amr + import numpy as np + + class HeatEquationModel(AMReXBaseModel): + _param_fields = [ + ('param', 'diffusion_coeff'), + ('param', 'init_amplitude'), + ('param', 'init_width'), + ] + + _output_fields = [ + ('output', 'max_temp'), + ('output', 'mean_temp'), + ('output', 'std_temp'), + ('output', 'total_energy'), + ] + + def evolve(self, param_set: np.ndarray): + """Run simulation with given parameters.""" + from main import main + phi_new, geom = main( + diffusion_coeff=float(param_set[0]), + init_amplitude=float(param_set[1]), + init_width=float(param_set[2]), + plot_files_output=False, + verbose=0 + ) + varnames = amr.Vector_string(['phi']) + return phi_new, varnames, geom + + def postprocess(self, sim_state) -> np.ndarray: + """Extract quantities of interest.""" + multifab, varnames, geom = sim_state + max_val = multifab.max(comp=0, local=False) + sum_val = multifab.sum(comp=0, local=False) + mean_val = sum_val / multifab.box_array().numPts + # Calculate std dev... + return np.array([max_val, mean_val, std_val, sum_val]) + +See ``Case-3/HeatEquationModel.py`` for the complete implementation. + Summary ------- From 8646b557bf13ba8fc2f4e7c975d642eb349fcaf5 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 09:21:03 -0700 Subject: [PATCH 102/142] Update integration description --- Docs/source/HeatEquation_UQ.rst | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 4e15c9f1..f4e0484b 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -502,6 +502,83 @@ Create a ``HeatEquationModel.py`` that inherits from ``AMReXBaseModel``: See ``Case-3/HeatEquationModel.py`` for the complete implementation. +Configure PyTUQ Parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PyTUQ requires configuration files specifying uncertain parameters and output quantities of interest. For BASH-driven workflows (Case-1), create these files directly: + +**Parameter Configuration (param_margpc.txt)** + +Specify mean and standard deviation for each uncertain parameter (one per line): + +.. code-block:: bash + + echo "1 0.25 " > param_margpc.txt # diffusion_coeff: mean=1.0, std=0.25 + echo "1 0.25" >> param_margpc.txt # init_amplitude: mean=1.0, std=0.25 + echo "0.01 0.0025" >> param_margpc.txt # init_width: mean=0.01, std=0.0025 + +**Parameter Names (pnames.txt)** + +List parameter names matching your AMReX ParmParse inputs: + +.. code-block:: bash + + echo "diffusion_coeff" > pnames.txt + echo "init_amplitude" >> pnames.txt + echo "init_width" >> pnames.txt + +**Output Names (outnames.txt)** + +Specify quantities of interest to extract from simulation outputs: + +.. code-block:: bash + + echo "max_temp" > outnames.txt + echo "mean_temp" >> outnames.txt + echo "std_temp" >> outnames.txt + echo "total_energy" >> outnames.txt + +**Polynomial Chaos Configuration** + +Set the polynomial chaos type and order: + +.. code-block:: bash + + PCTYPE="HG" # Hermite-Gaussian for normal distributions + ORDER=1 # First-order polynomial chaos + NSAM=111 # Number of samples (depends on dimensionality and order) + +.. note:: + + **Polynomial Chaos Types:** + + - ``HG`` (Hermite-Gaussian): For normal/Gaussian distributions + - ``LU`` (Legendre-Uniform): For uniform distributions + - ``LG`` (Laguerre-Gamma): For gamma distributions + + **Sample Count:** For ``d`` dimensions and order ``p``, you need at least ``(p+d)!/(p!*d!)`` samples. For 3D with order 1: minimum 4 samples, recommended 111+. + +**PyTUQ Workflow Steps** + +The workflow consists of three stages: + +1. **Prepare PC basis** - Use ``pc_prep.py`` to initialize polynomial chaos basis functions based on parameter distributions + +2. **Sample parameter space** - Use ``pc_sam.py`` to generate parameter samples (creates ``qsam.txt``) + +3. **Fit surrogate model** - Use ``pc_fit.py`` to construct polynomial chaos expansion from simulation outputs + +.. note:: + + **Case-2 Flexibility:** + + Case-2 (Python-wrapped C++) supports both approaches: + + - **BASH file configuration** (like Case-1): Use text files (``param_margpc.txt``, ``pnames.txt``, ``outnames.txt``) with a ``model.x`` wrapper script that specifies which C++ executable and inputs file to use + - **Python metadata**: Define parameter distributions in the model class's ``_get_field_info()`` method + + Case-3 (native PyAMReX) requires Python metadata since there's no separate executable to wrap. + Summary ------- From 3c33888d9d271dd6292a93faa440b5de4e4becd9 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 09:36:16 -0700 Subject: [PATCH 103/142] Subpages --- Docs/source/HeatEquation_UQ.rst | 383 +----------------- .../HeatEquation_UQ_ExtendingTutorial.rst | 290 +++++++++++++ .../HeatEquation_UQ_MathematicalDetails.rst | 88 ++++ 3 files changed, 385 insertions(+), 376 deletions(-) create mode 100644 Docs/source/HeatEquation_UQ_ExtendingTutorial.rst create mode 100644 Docs/source/HeatEquation_UQ_MathematicalDetails.rst diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index f4e0484b..ecea5b5c 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -2,8 +2,8 @@ .. _pytuq_quickstart: -PyTUQ -===== +AMReX-pytuq +=========== .. admonition:: **Time to Complete**: 15-20 minutes :class: note @@ -203,381 +203,12 @@ C++ AMReX on Perlmutter `_ for details. -Mathematical Details --------------------- - -Parameter Sensitivity via Polynomial Chaos -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -In this example, PyTUQ constructs polynomial chaos expansions to analyze how uncertain physical parameters affect simulation outputs in a heat diffusion problem. - -The Heat Equation -^^^^^^^^^^^^^^^^^ - -The governing equation for this tutorial is the heat diffusion equation: - -.. math:: - - \frac{\partial T}{\partial t} = D \nabla^2 T + S(x,y,z) - -where: - -- :math:`T` is the temperature field -- :math:`D` is the diffusion coefficient (``diffusion_coeff``) -- :math:`S(x,y,z)` is an optional source term (not used in this example) - -The initial temperature profile is a Gaussian centered at (0.5, 0.5, 0.5): - -.. math:: - - T(x,y,z,t=0) = 1 + A \exp\left(-\frac{r^2}{w^2}\right) - -where: - -- :math:`A` is the initial amplitude (``init_amplitude``) -- :math:`w^2` is the initial width parameter (``init_width``) -- :math:`r^2 = (x-0.5)^2 + (y-0.5)^2 + (z-0.5)^2` - -Uncertain Input Parameters -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The three uncertain parameters in this analysis are: - -1. **diffusion_coeff** (:math:`D`): Controls how fast heat spreads through the domain - - - Mean: 1.0 m²/s - - Standard deviation: 0.25 m²/s - - Range: [0.25, 1.75] m²/s - -2. **init_amplitude** (:math:`A`): Peak temperature above baseline - - - Mean: 1.0 K - - Standard deviation: 0.25 K - - Range: [0.25, 1.75] K - -3. **init_width** (:math:`w^2`): Controls spread of initial temperature profile - - - Mean: 0.01 m² - - Standard deviation: 0.0025 m² - - Range: [0.0025, 0.0175] m² - -These parameters are specified in the AMReX inputs file and read using ``ParmParse::query()`` (see ``main.cpp`` lines 100-111). - -Quantities of Interest (Outputs) -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The simulation extracts four statistical quantities at the final timestep: - -1. **max_temp**: Maximum temperature in the domain -2. **mean_temp**: Average temperature across all cells -3. **std_temp**: Standard deviation of temperature -4. **total_energy**: Sum of temperature values (proportional to total thermal energy) - -These outputs are computed in ``main.cpp`` (lines 293-299) and written to the datalog file. - -PyTUQ Workflow -^^^^^^^^^^^^^^ - -PyTUQ uses polynomial chaos expansion to construct a surrogate model: - -1. **Parameter sampling**: Generate sample points in the 3D parameter space based on the specified distributions -2. **Forward simulations**: Run the AMReX heat equation solver for each parameter set -3. **Surrogate construction**: Fit polynomial chaos coefficients to map inputs → outputs -4. **Sensitivity analysis**: Compute Sobol indices to identify which parameters most influence each output - -The connection is: - -- **Inputs**: ParmParse parameters (``diffusion_coeff``, ``init_amplitude``, ``init_width``) specified in ``inputs`` file or command line -- **Outputs**: Quantities of interest extracted from datalog files or direct Python access to MultiFabs - -Extending this tutorial to other applications ---------------------------------------------- - -Identify / Add input parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -**Implementation Steps:** - -1. Add physics parameters to main.cpp declarations: - - .. code-block:: cpp - - amrex::Real diffusion_coeff; - amrex::Real init_amplitude; - amrex::Real init_width; - -2. Read parameters from inputs file: - - .. code-block:: cpp - - diffusion_coeff = 1.0; - pp.query("diffusion_coeff", diffusion_coeff); - - init_amplitude = 1.0; - pp.query("init_amplitude", init_amplitude); - - init_width = 0.01; - pp.query("init_width", init_width); - -3. Use parameters in initial conditions and evolution: - - .. code-block:: cpp - - // Initial conditions - amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; - phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); - - // Evolution - phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; - -The simplest output extraction method is described here: - -Output example: C++ Datalog -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. admonition:: When to use - :class: tip - - Choose when you have centralized output locations and want simple I/O. - -**Implementation Steps:** - -1. Add datalog configuration to C++ code: - - .. code-block:: cpp - - const int datwidth = 14; - const int datprecision = 6; - int datalog_int = -1; - bool datalog_final = true; - pp.query("datalog_int", datalog_int); - -2. Write statistics to datalog.txt: - - .. code-block:: cpp - - // Check if we should write datalog - bool write_datalog = false; - if (datalog_final && step == nsteps) { - write_datalog = true; // Write final step - } else if (datalog_int > 0 && step % datalog_int == 0) { - write_datalog = true; // Write every datalog_int steps - } - - if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { - std::ofstream datalog("datalog.txt", std::ios::app); - - amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); - amrex::Real max_temperature = phi_new.max(0); - amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; - amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; - - datalog << time << " " << max_temperature << " " << std_temperature << " " << step << std::endl; - } - - .. note:: - - Some AMReX codes already have datalog capabilities: - - - For existing datalogs, you may only need to ``tail -n 1 datalog`` for the final values - - For AMR codes, use the built-in DataLog: - - .. code-block:: cpp - - // Assuming 'amr' is your Amr object - if (amrex::ParallelDescriptor::IOProcessor()) { - amr.DataLog(0) << "# time max_temperature mean_temp" << std::endl; - amr.DataLog(0) << time << " " << max_temperature << " " << mean_temp << std::endl; - } - -Extending to PyAMReX Applications -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -For PyAMReX applications, adapt your existing ``main.py`` to enable UQ parameter sweeps: - -**1. Modify main() function signature** - -Add UQ parameters as function arguments: - -.. code-block:: python - :caption: Original - - def main(n_cell, max_grid_size, nsteps, plot_int, dt): - -.. code-block:: python - :caption: With UQ parameters - - def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, - plot_int: int = 100, dt: float = 1e-5, plot_files_output: bool = False, - verbose: int = 1, diffusion_coeff: float = 1.0, init_amplitude: float = 1.0, - init_width: float = 0.01) -> Tuple[amr.MultiFab, amr.Geometry]: - -**2. Use parameters in physics** - -Replace hardcoded values with function parameters: - -.. code-block:: python - :caption: Original initial condition (main.py:117-118) - - rsquared = ((x[:, xp.newaxis, xp.newaxis] - 0.5)**2 - + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 - + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / 0.01 - phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + xp.exp(-rsquared) - -.. code-block:: python - :caption: Parameterized - - rsquared = ((x[:, xp.newaxis, xp.newaxis] - 0.5)**2 - + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 - + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / init_width - phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + init_amplitude * xp.exp(-rsquared) - -.. code-block:: python - :caption: Original evolution (main.py:143-144) - - phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( - phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] - + dt*((phiOld[:, ngz:-ngz, ngy:-ngy, ngx+1:hix-ngx+1] - ... ) / dx[0]**2)) - -.. code-block:: python - :caption: With diffusion coefficient - - phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( - phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] - + dt * diffusion_coeff * ((phiOld[:, ngz:-ngz, ngy:-ngy, ngx+1:hix-ngx+1] - ... ) / dx[0]**2)) - -**3. Return simulation state** - -Modify the main function to return MultiFab and Geometry: - -.. code-block:: python - - return phi_new, geom - -**4. Create PyTUQ model wrapper** - -Create a ``HeatEquationModel.py`` that inherits from ``AMReXBaseModel``: - -.. code-block:: python - - from AMReXBaseModel import AMReXBaseModel - import amrex.space3d as amr - import numpy as np - - class HeatEquationModel(AMReXBaseModel): - _param_fields = [ - ('param', 'diffusion_coeff'), - ('param', 'init_amplitude'), - ('param', 'init_width'), - ] - - _output_fields = [ - ('output', 'max_temp'), - ('output', 'mean_temp'), - ('output', 'std_temp'), - ('output', 'total_energy'), - ] - - def evolve(self, param_set: np.ndarray): - """Run simulation with given parameters.""" - from main import main - phi_new, geom = main( - diffusion_coeff=float(param_set[0]), - init_amplitude=float(param_set[1]), - init_width=float(param_set[2]), - plot_files_output=False, - verbose=0 - ) - varnames = amr.Vector_string(['phi']) - return phi_new, varnames, geom - - def postprocess(self, sim_state) -> np.ndarray: - """Extract quantities of interest.""" - multifab, varnames, geom = sim_state - max_val = multifab.max(comp=0, local=False) - sum_val = multifab.sum(comp=0, local=False) - mean_val = sum_val / multifab.box_array().numPts - # Calculate std dev... - return np.array([max_val, mean_val, std_val, sum_val]) - -See ``Case-3/HeatEquationModel.py`` for the complete implementation. - -Configure PyTUQ Parameters -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -PyTUQ requires configuration files specifying uncertain parameters and output quantities of interest. For BASH-driven workflows (Case-1), create these files directly: - -**Parameter Configuration (param_margpc.txt)** - -Specify mean and standard deviation for each uncertain parameter (one per line): - -.. code-block:: bash - - echo "1 0.25 " > param_margpc.txt # diffusion_coeff: mean=1.0, std=0.25 - echo "1 0.25" >> param_margpc.txt # init_amplitude: mean=1.0, std=0.25 - echo "0.01 0.0025" >> param_margpc.txt # init_width: mean=0.01, std=0.0025 - -**Parameter Names (pnames.txt)** - -List parameter names matching your AMReX ParmParse inputs: - -.. code-block:: bash - - echo "diffusion_coeff" > pnames.txt - echo "init_amplitude" >> pnames.txt - echo "init_width" >> pnames.txt - -**Output Names (outnames.txt)** - -Specify quantities of interest to extract from simulation outputs: - -.. code-block:: bash - - echo "max_temp" > outnames.txt - echo "mean_temp" >> outnames.txt - echo "std_temp" >> outnames.txt - echo "total_energy" >> outnames.txt - -**Polynomial Chaos Configuration** - -Set the polynomial chaos type and order: - -.. code-block:: bash - - PCTYPE="HG" # Hermite-Gaussian for normal distributions - ORDER=1 # First-order polynomial chaos - NSAM=111 # Number of samples (depends on dimensionality and order) - -.. note:: - - **Polynomial Chaos Types:** - - - ``HG`` (Hermite-Gaussian): For normal/Gaussian distributions - - ``LU`` (Legendre-Uniform): For uniform distributions - - ``LG`` (Laguerre-Gamma): For gamma distributions - - **Sample Count:** For ``d`` dimensions and order ``p``, you need at least ``(p+d)!/(p!*d!)`` samples. For 3D with order 1: minimum 4 samples, recommended 111+. - -**PyTUQ Workflow Steps** - -The workflow consists of three stages: - -1. **Prepare PC basis** - Use ``pc_prep.py`` to initialize polynomial chaos basis functions based on parameter distributions - -2. **Sample parameter space** - Use ``pc_sam.py`` to generate parameter samples (creates ``qsam.txt``) - -3. **Fit surrogate model** - Use ``pc_fit.py`` to construct polynomial chaos expansion from simulation outputs - -.. note:: - - **Case-2 Flexibility:** - - Case-2 (Python-wrapped C++) supports both approaches: - - - **BASH file configuration** (like Case-1): Use text files (``param_margpc.txt``, ``pnames.txt``, ``outnames.txt``) with a ``model.x`` wrapper script that specifies which C++ executable and inputs file to use - - **Python metadata**: Define parameter distributions in the model class's ``_get_field_info()`` method +.. toctree:: + :maxdepth: 1 + :caption: Additional Topics - Case-3 (native PyAMReX) requires Python metadata since there's no separate executable to wrap. + HeatEquation_UQ_MathematicalDetails + HeatEquation_UQ_ExtendingTutorial Summary ------- diff --git a/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst b/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst new file mode 100644 index 00000000..37469a64 --- /dev/null +++ b/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst @@ -0,0 +1,290 @@ +.. _pytuq_extending_tutorial: + +Extending this tutorial to other applications +============================================== + +Identify / Add input parameters +-------------------------------- + +**Implementation Steps:** + +1. Add physics parameters to main.cpp declarations: + + .. code-block:: cpp + + amrex::Real diffusion_coeff; + amrex::Real init_amplitude; + amrex::Real init_width; + +2. Read parameters from inputs file: + + .. code-block:: cpp + + diffusion_coeff = 1.0; + pp.query("diffusion_coeff", diffusion_coeff); + + init_amplitude = 1.0; + pp.query("init_amplitude", init_amplitude); + + init_width = 0.01; + pp.query("init_width", init_width); + +3. Use parameters in initial conditions and evolution: + + .. code-block:: cpp + + // Initial conditions + amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; + phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); + + // Evolution + phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; + +The simplest output extraction method is described here: + +Output example: C++ Datalog +---------------------------- + +.. admonition:: When to use + :class: tip + + Choose when you have centralized output locations and want simple I/O. + +**Implementation Steps:** + +1. Add datalog configuration to C++ code: + + .. code-block:: cpp + + const int datwidth = 14; + const int datprecision = 6; + int datalog_int = -1; + bool datalog_final = true; + pp.query("datalog_int", datalog_int); + +2. Write statistics to datalog.txt: + + .. code-block:: cpp + + // Check if we should write datalog + bool write_datalog = false; + if (datalog_final && step == nsteps) { + write_datalog = true; // Write final step + } else if (datalog_int > 0 && step % datalog_int == 0) { + write_datalog = true; // Write every datalog_int steps + } + + if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { + std::ofstream datalog("datalog.txt", std::ios::app); + + amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); + amrex::Real max_temperature = phi_new.max(0); + amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; + amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; + + datalog << time << " " << max_temperature << " " << std_temperature << " " << step << std::endl; + } + + .. note:: + + Some AMReX codes already have datalog capabilities: + + - For existing datalogs, you may only need to ``tail -n 1 datalog`` for the final values + - For AMR codes, use the built-in DataLog: + + .. code-block:: cpp + + // Assuming 'amr' is your Amr object + if (amrex::ParallelDescriptor::IOProcessor()) { + amr.DataLog(0) << "# time max_temperature mean_temp" << std::endl; + amr.DataLog(0) << time << " " << max_temperature << " " << mean_temp << std::endl; + } + +Extending to PyAMReX Applications +---------------------------------- + +For PyAMReX applications, adapt your existing ``main.py`` to enable UQ parameter sweeps: + +**1. Modify main() function signature** + +Add UQ parameters as function arguments: + +.. code-block:: python + :caption: Original + + def main(n_cell, max_grid_size, nsteps, plot_int, dt): + +.. code-block:: python + :caption: With UQ parameters + + def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, + plot_int: int = 100, dt: float = 1e-5, plot_files_output: bool = False, + verbose: int = 1, diffusion_coeff: float = 1.0, init_amplitude: float = 1.0, + init_width: float = 0.01) -> Tuple[amr.MultiFab, amr.Geometry]: + +**2. Use parameters in physics** + +Replace hardcoded values with function parameters: + +.. code-block:: python + :caption: Original initial condition (main.py:117-118) + + rsquared = ((x[:, xp.newaxis, xp.newaxis] - 0.5)**2 + + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 + + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / 0.01 + phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + xp.exp(-rsquared) + +.. code-block:: python + :caption: Parameterized + + rsquared = ((x[:, xp.newaxis, xp.newaxis] - 0.5)**2 + + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 + + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / init_width + phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + init_amplitude * xp.exp(-rsquared) + +.. code-block:: python + :caption: Original evolution (main.py:143-144) + + phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( + phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + + dt*((phiOld[:, ngz:-ngz, ngy:-ngy, ngx+1:hix-ngx+1] - ... ) / dx[0]**2)) + +.. code-block:: python + :caption: With diffusion coefficient + + phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( + phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + + dt * diffusion_coeff * ((phiOld[:, ngz:-ngz, ngy:-ngy, ngx+1:hix-ngx+1] - ... ) / dx[0]**2)) + +**3. Return simulation state** + +Modify the main function to return MultiFab and Geometry: + +.. code-block:: python + + return phi_new, geom + +**4. Create PyTUQ model wrapper** + +Create a ``HeatEquationModel.py`` that inherits from ``AMReXBaseModel``: + +.. code-block:: python + + from AMReXBaseModel import AMReXBaseModel + import amrex.space3d as amr + import numpy as np + + class HeatEquationModel(AMReXBaseModel): + _param_fields = [ + ('param', 'diffusion_coeff'), + ('param', 'init_amplitude'), + ('param', 'init_width'), + ] + + _output_fields = [ + ('output', 'max_temp'), + ('output', 'mean_temp'), + ('output', 'std_temp'), + ('output', 'total_energy'), + ] + + def evolve(self, param_set: np.ndarray): + """Run simulation with given parameters.""" + from main import main + phi_new, geom = main( + diffusion_coeff=float(param_set[0]), + init_amplitude=float(param_set[1]), + init_width=float(param_set[2]), + plot_files_output=False, + verbose=0 + ) + varnames = amr.Vector_string(['phi']) + return phi_new, varnames, geom + + def postprocess(self, sim_state) -> np.ndarray: + """Extract quantities of interest.""" + multifab, varnames, geom = sim_state + max_val = multifab.max(comp=0, local=False) + sum_val = multifab.sum(comp=0, local=False) + mean_val = sum_val / multifab.box_array().numPts + # Calculate std dev... + return np.array([max_val, mean_val, std_val, sum_val]) + +See ``Case-3/HeatEquationModel.py`` for the complete implementation. + +Configure PyTUQ Parameters +--------------------------- + +PyTUQ requires configuration files specifying uncertain parameters and output quantities of interest. For BASH-driven workflows (Case-1), create these files directly: + +**Parameter Configuration (param_margpc.txt)** + +Specify mean and standard deviation for each uncertain parameter (one per line): + +.. code-block:: bash + + echo "1 0.25 " > param_margpc.txt # diffusion_coeff: mean=1.0, std=0.25 + echo "1 0.25" >> param_margpc.txt # init_amplitude: mean=1.0, std=0.25 + echo "0.01 0.0025" >> param_margpc.txt # init_width: mean=0.01, std=0.0025 + +**Parameter Names (pnames.txt)** + +List parameter names matching your AMReX ParmParse inputs: + +.. code-block:: bash + + echo "diffusion_coeff" > pnames.txt + echo "init_amplitude" >> pnames.txt + echo "init_width" >> pnames.txt + +**Output Names (outnames.txt)** + +Specify quantities of interest to extract from simulation outputs: + +.. code-block:: bash + + echo "max_temp" > outnames.txt + echo "mean_temp" >> outnames.txt + echo "std_temp" >> outnames.txt + echo "total_energy" >> outnames.txt + +**Polynomial Chaos Configuration** + +Set the polynomial chaos type and order: + +.. code-block:: bash + + PCTYPE="HG" # Hermite-Gaussian for normal distributions + ORDER=1 # First-order polynomial chaos + NSAM=111 # Number of samples (depends on dimensionality and order) + +.. note:: + + **Polynomial Chaos Types:** + + - ``HG`` (Hermite-Gaussian): For normal/Gaussian distributions + - ``LU`` (Legendre-Uniform): For uniform distributions + - ``LG`` (Laguerre-Gamma): For gamma distributions + + **Sample Count:** For ``d`` dimensions and order ``p``, you need at least ``(p+d)!/(p!*d!)`` samples. For 3D with order 1: minimum 4 samples, recommended 111+. + +**PyTUQ Workflow Steps** + +The workflow consists of three stages: + +1. **Prepare PC basis** - Use ``pc_prep.py`` to initialize polynomial chaos basis functions based on parameter distributions + +2. **Sample parameter space** - Use ``pc_sam.py`` to generate parameter samples (creates ``qsam.txt``) + +3. **Fit surrogate model** - Use ``pc_fit.py`` to construct polynomial chaos expansion from simulation outputs + +.. note:: + + **Case-2 Flexibility:** + + Case-2 (Python-wrapped C++) supports both approaches: + + - **BASH file configuration** (like Case-1): Use text files (``param_margpc.txt``, ``pnames.txt``, ``outnames.txt``) with a ``model.x`` wrapper script that specifies which C++ executable and inputs file to use + - **Python metadata**: Define parameter distributions in the model class's ``_get_field_info()`` method + + Case-3 (native PyAMReX) requires Python metadata since there's no separate executable to wrap. diff --git a/Docs/source/HeatEquation_UQ_MathematicalDetails.rst b/Docs/source/HeatEquation_UQ_MathematicalDetails.rst new file mode 100644 index 00000000..3328ec1f --- /dev/null +++ b/Docs/source/HeatEquation_UQ_MathematicalDetails.rst @@ -0,0 +1,88 @@ +.. _pytuq_mathematical_details: + +Mathematical Details +==================== + +Parameter Sensitivity via Polynomial Chaos +------------------------------------------- + +In this example, PyTUQ constructs polynomial chaos expansions to analyze how uncertain physical parameters affect simulation outputs in a heat diffusion problem. + +The Heat Equation +^^^^^^^^^^^^^^^^^ + +The governing equation for this tutorial is the heat diffusion equation: + +.. math:: + + \frac{\partial T}{\partial t} = D \nabla^2 T + S(x,y,z) + +where: + +- :math:`T` is the temperature field +- :math:`D` is the diffusion coefficient (``diffusion_coeff``) +- :math:`S(x,y,z)` is an optional source term (not used in this example) + +The initial temperature profile is a Gaussian centered at (0.5, 0.5, 0.5): + +.. math:: + + T(x,y,z,t=0) = 1 + A \exp\left(-\frac{r^2}{w^2}\right) + +where: + +- :math:`A` is the initial amplitude (``init_amplitude``) +- :math:`w^2` is the initial width parameter (``init_width``) +- :math:`r^2 = (x-0.5)^2 + (y-0.5)^2 + (z-0.5)^2` + +Uncertain Input Parameters +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The three uncertain parameters in this analysis are: + +1. **diffusion_coeff** (:math:`D`): Controls how fast heat spreads through the domain + + - Mean: 1.0 m²/s + - Standard deviation: 0.25 m²/s + - Range: [0.25, 1.75] m²/s + +2. **init_amplitude** (:math:`A`): Peak temperature above baseline + + - Mean: 1.0 K + - Standard deviation: 0.25 K + - Range: [0.25, 1.75] K + +3. **init_width** (:math:`w^2`): Controls spread of initial temperature profile + + - Mean: 0.01 m² + - Standard deviation: 0.0025 m² + - Range: [0.0025, 0.0175] m² + +These parameters are specified in the AMReX inputs file and read using ``ParmParse::query()`` (see ``main.cpp`` lines 100-111). + +Quantities of Interest (Outputs) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The simulation extracts four statistical quantities at the final timestep: + +1. **max_temp**: Maximum temperature in the domain +2. **mean_temp**: Average temperature across all cells +3. **std_temp**: Standard deviation of temperature +4. **total_energy**: Sum of temperature values (proportional to total thermal energy) + +These outputs are computed in ``main.cpp`` (lines 293-299) and written to the datalog file. + +PyTUQ Workflow +^^^^^^^^^^^^^^ + +PyTUQ uses polynomial chaos expansion to construct a surrogate model: + +1. **Parameter sampling**: Generate sample points in the 3D parameter space based on the specified distributions +2. **Forward simulations**: Run the AMReX heat equation solver for each parameter set +3. **Surrogate construction**: Fit polynomial chaos coefficients to map inputs → outputs +4. **Sensitivity analysis**: Compute Sobol indices to identify which parameters most influence each output + +The connection is: + +- **Inputs**: ParmParse parameters (``diffusion_coeff``, ``init_amplitude``, ``init_width``) specified in ``inputs`` file or command line +- **Outputs**: Quantities of interest extracted from datalog files or direct Python access to MultiFabs From d30c83ffff736f9240b421fb118801f801f32a55 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 09:51:15 -0700 Subject: [PATCH 104/142] Rework headers --- Docs/source/HeatEquation_UQ.rst | 4 +++- Docs/source/HeatEquation_UQ_ExtendingTutorial.rst | 6 ++++-- Docs/source/HeatEquation_UQ_MathematicalDetails.rst | 3 --- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index ecea5b5c..2d362b47 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -203,9 +203,11 @@ C++ AMReX on Perlmutter `_ for details. +Customizing the Workflow +------------------------ + .. toctree:: :maxdepth: 1 - :caption: Additional Topics HeatEquation_UQ_MathematicalDetails HeatEquation_UQ_ExtendingTutorial diff --git a/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst b/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst index 37469a64..de62239b 100644 --- a/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst +++ b/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst @@ -1,7 +1,9 @@ .. _pytuq_extending_tutorial: -Extending this tutorial to other applications -============================================== +Extending this approach from existing applications +================================================== + +This section describes how HeatEquation_Simple and the pyamrex HeatEquation from HeatEquation_EX0_C can be modified step by step to use with pytuq to give a framework for making a similar extension for other AMReX codes. Identify / Add input parameters -------------------------------- diff --git a/Docs/source/HeatEquation_UQ_MathematicalDetails.rst b/Docs/source/HeatEquation_UQ_MathematicalDetails.rst index 3328ec1f..3580bccd 100644 --- a/Docs/source/HeatEquation_UQ_MathematicalDetails.rst +++ b/Docs/source/HeatEquation_UQ_MathematicalDetails.rst @@ -1,8 +1,5 @@ .. _pytuq_mathematical_details: -Mathematical Details -==================== - Parameter Sensitivity via Polynomial Chaos ------------------------------------------- From b687e3da4c14bf3c53ef6777f37e3ec00587251e Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 09:54:28 -0700 Subject: [PATCH 105/142] Tweak open and add link --- Docs/source/HeatEquation_UQ.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 2d362b47..d7c382a0 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -38,6 +38,12 @@ Installation pip install -r requirements.txt pip install . +.. note:: + + **Comprehensive Installation:** + + For a detailed installation script that includes AMReX, pyAMReX, and PyTUQ setup in a conda environment, see ``GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh`` + Examples -------- @@ -45,6 +51,7 @@ C++ AMReX + PyTUQ (BASH driven) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. dropdown:: Build and Run + :open: **Prerequisites**: AMReX compiled with MPI support @@ -161,10 +168,6 @@ PyAMReX + PyTUQ Perlmutter Deployment --------------------- -.. note:: - - Module setup required: ``module load python cuda`` - C++ AMReX on Perlmutter ~~~~~~~~~~~~~~~~~~~~~~~~ From 3f8abdb345f278dae5e2fb3c050145a40bd4cd68 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 10:05:12 -0700 Subject: [PATCH 106/142] Point out section --- Docs/source/HeatEquation_UQ.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index d7c382a0..9aa2e1d4 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -13,8 +13,8 @@ AMReX-pytuq - Run AMReX + PyTUQ examples - Deploy on Perlmutter -Motivation ----------- +Overview +-------- AMReX simulations deliver high-fidelity, accurate results for complex physics problems, but parameter studies and uncertainty quantification can require hundreds or thousands of runs—making comprehensive analysis computationally prohibitive. @@ -22,9 +22,9 @@ This tutorial demonstrates how to improve efficiency without sacrificing accurac PyTUQ (Python interface to the UQ Toolkit) provides specialized tools for surrogate construction and global sensitivity analysis, enabling rapid parameter space exploration and dimensionality reduction for scientific applications. -We demonstrate how to integrate PyTUQ with your AMReX application through three practical workflows: C++ executables with file-based I/O (Case-1), Python-wrapped C++ codes (Case-2), and native PyAMReX applications (Case-3). +We demonstrate how to integrate PyTUQ with your AMReX application through three practical workflows: C++ executables managed with gnu parallel (Case-1), Python-driven C++ executables with bash run management (Case-2), and native PyAMReX applications (Case-3). -Located in ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ``, this example analyzes a heat equation solver to illustrate the complete forward UQ workflow from parameter sampling through sensitivity analysis. +Located in ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ``, this example analyzes a heat equation solver to illustrate the complete forward UQ workflow from parameter sampling through sensitivity analysis. After running the provided examples, the Customizing the Workflow section explains the underlying problem setup and provides step-by-step guidance for adapting this workflow to your own AMReX application Installation ------------ From 4c905242c4fb41a69ac4ed73afb58d5e9fb7ac1f Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 10:21:15 -0700 Subject: [PATCH 107/142] Add more specifics about run management to docs --- Docs/source/HeatEquation_UQ.rst | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 9aa2e1d4..94e77e26 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -53,7 +53,10 @@ C++ AMReX + PyTUQ (BASH driven) .. dropdown:: Build and Run :open: - **Prerequisites**: AMReX compiled with MPI support + **Prerequisites**: + + - AMReX and pytuq cloned at the same directory level as amrex-tutorials + - GNU Parallel for task management: ``sudo apt-get install parallel`` .. code-block:: bash :caption: Build C++ example @@ -116,6 +119,20 @@ C++ AMReX + PyTUQ (python driven) python ./ex_pcgsa.py + .. note:: + + **Offline Workflow (similar to Case-1):** + + Case-2 can also be run in offline mode where you manually generate training data, then fit the surrogate model. This is useful for running simulations on HPC systems and post-processing locally: + + .. code-block:: bash + :caption: Generate training data offline + + # Call model.x on parameter samples to generate outputs + ./model.x ptrain.txt ytrain.txt + + The ``model.x`` wrapper script manages calling your C++ executable for each parameter set and collecting the outputs. After generating ``ytrain.txt``, use PyTUQ's ``pc_fit.py`` to construct the surrogate model. + PyAMReX + PyTUQ ~~~~~~~~~~~~~~~ From 32e222e06ccd2f4cc6f4b7754c010c6e0c101333 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 10:42:51 -0700 Subject: [PATCH 108/142] Add links and references --- Docs/source/HeatEquation_UQ.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 94e77e26..7b588562 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -223,6 +223,18 @@ C++ AMReX on Perlmutter `_ for details. +.. note:: + + **Advanced Workflow Management:** + + For more complex UQ workflows requiring sophisticated task management, consider these strategies: + + - **SLURM Job Arrays**: For running many similar parameter sweep jobs - `NERSC Job Arrays Documentation `_ + - **Hydra Submitit Launcher**: For configuration-driven HPC job submission - `Hydra Submitit Usage `_ + - **libEnsemble**: For dynamic ensemble management and adaptive sampling - `libEnsemble Platforms Guide `_ + + See the `NERSC Workflow Documentation `_ for the latest recommendations on workflow management strategies. + Customizing the Workflow ------------------------ @@ -244,12 +256,22 @@ Summary Additional Resources -------------------- +**PyTUQ Resources:** + - `PyTUQ Documentation `_ - `PyTUQ Examples directory `_ +- eebaill, ksargsyan, & Bert Debusschere. (2025). sandialabs/pytuq: v1.0.0z (v1.0.0z). Zenodo. https://doi.org/10.5281/zenodo.17110054 + +**AMReX Resources:** + - `AMReX Documentation `_ - `pyAMReX Documentation `_ - :ref:`_guided_heat` - Base tutorial this builds upon +**Uncertainty Quantification Theory:** + +- Ghanem, Roger, David Higdon, and Houman Owhadi, eds. *Handbook of Uncertainty Quantification*. Vol. 6. New York: Springer, 2017. (For workflow, plotting, and analysis specifics) + .. seealso:: For complete working examples of the ``outputs = model(inputs)`` pattern, see: From 9a2d53a7b9e344633c04aceddedaa87a3444906b Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 10:58:41 -0700 Subject: [PATCH 109/142] Extra output example --- .../HeatEquation_UQ_ExtendingTutorial.rst | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst b/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst index de62239b..4cf8c22a 100644 --- a/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst +++ b/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst @@ -102,6 +102,119 @@ Output example: C++ Datalog amr.DataLog(0) << time << " " << max_temperature << " " << mean_temp << std::endl; } +Output example: Plotfiles with AMReX Tools +------------------------------------------- + +.. admonition:: When to use + :class: tip + + Choose when you already write plotfiles and want to use AMReX's built-in analysis tools. This works well for offline workflows (like Case-1) where simulations run on HPC and post-processing happens locally. + +AMReX provides several compiled tools in ``amrex/Tools/Plotfile/`` for extracting information from plotfiles without writing custom code. + +**Key Tools:** + +- ``fextrema``: Extract minimum and maximum values for each variable +- ``fextract``: Extract 1D slices through plotfiles +- ``fvarnames``: List variable names in a plotfile +- ``fcompare``: Compare two plotfiles + +**Implementation Steps:** + +1. Ensure your simulation writes plotfiles: + + .. code-block:: cpp + + // In your main time-stepping loop + if (plot_int > 0 && (step % plot_int == 0 || step == nsteps)) { + const std::string& pltfile = amrex::Concatenate("plt", step, 5); + WriteSingleLevelPlotfile(pltfile, phi_new, varnames, geom, time, step); + } + +2. Build the AMReX plotfile tools (one-time setup): + + .. code-block:: bash + + cd $AMREX_HOME/Tools/Plotfile + make -j4 + + This creates executables like ``fextrema.gnu.ex``, ``fextract.gnu.ex``, etc. + +3. Use ``fextrema`` to extract min/max values: + + .. code-block:: bash + + # Extract extrema for all variables + ./fextrema.gnu.ex plt00100/ + + # Output: + # plotfile = plt00100/ + # time = 0.001000000000000002 + # variables minimum value maximum value + # phi 1 1.5801622094 + +4. Parse the output in your UQ workflow script: + + .. code-block:: bash + :caption: In wf_uqpc.x or model.x wrapper + + #!/bin/bash + # Run simulation + ./heat_equation_exec inputs diffusion_coeff=$1 init_amplitude=$2 + + # Option 1: Analyze a specific plotfile (if you know the timestep) + FINAL_PLT="plt00100" + + # Option 2: Find the final plotfile dynamically + FINAL_PLT=$(ls -d plt[0-9]* | tail -1) + + # Option 3: Use a more sophisticated approach (adapted from AMReX-Astro/workflow) + # get_last_plotfile() { + # dir=${1:-.} # Use current directory if no argument + # num=${2:-1} # How far back to go (1 = most recent) + # ls -d $dir/plt[0-9]* 2>/dev/null | sort -t plt -k 2 -g | tail -$num | head -1 + # } + # FINAL_PLT=$(get_last_plotfile . 1) + + # Extract maximum temperature using fextrema + # Use tail -1 to get the last line, then awk to extract column 3 (ignoring variable name) + MAX_TEMP=$(./fextrema.gnu.ex $FINAL_PLT | tail -1 | awk '{print $3}') + + # Output in format expected by PyTUQ (space or tab separated) + echo "$MAX_TEMP" + + .. note:: + + **Parsing strategies:** + + - **Simple approach**: Use ``tail -1`` to get the last line of output, then extract columns 2 and 3 (min/max) with ``awk``, ignoring the variable name in column 1 + - **Robust approach**: Use ``fvarnames`` to get variable names first, then use ``fextrema -v varname`` to extract specific variables + - For multiple variables, process each separately and combine into space/tab-separated output + +5. Example: Extracting multiple quantities from plotfile: + + .. code-block:: bash + :caption: extract_from_plotfile.sh + + #!/bin/bash + PLOTFILE=$1 + FEXTREMA_PATH="$AMREX_HOME/Tools/Plotfile/fextrema.gnu.ex" + + # Extract max value (column 3 from last line) + MAX_VAL=$($FEXTREMA_PATH $PLOTFILE | tail -1 | awk '{print $3}') + + # Extract min value (column 2 from last line) + MIN_VAL=$($FEXTREMA_PATH $PLOTFILE | tail -1 | awk '{print $2}') + + # Output as space-separated values + echo "$MAX_VAL $MIN_VAL" + +.. seealso:: + + - AMReX Plotfile Tools: ``$AMREX_HOME/Tools/Plotfile/`` + - Each tool prints usage information when called with no arguments: ``./fextract.gnu.ex`` + - For robust plotfile/checkpoint selection utilities, see `AMReX-Astro workflow get_last_checkpoint `_ + Extending to PyAMReX Applications ---------------------------------- From e14427fd4454ac01a71ce6e505ccde19c8a25eba Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Fri, 10 Oct 2025 11:14:30 -0700 Subject: [PATCH 110/142] Add extra box with docs explaining some of the steps --- Docs/source/HeatEquation_UQ.rst | 69 +++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 7b588562..b29b6e24 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -69,6 +69,75 @@ C++ AMReX + PyTUQ (BASH driven) ./wf_uqpc.x +.. dropdown:: Understanding GNU Parallel Workflow Pattern + + After PyTUQ scripts generate an input parameter file (``ptrain.txt``), the Case-1 workflow uses GNU Parallel to run multiple simulations efficiently and collect outputs into a results file (``ytrain.txt``) that PyTUQ can use for surrogate model fitting. Here's how the pattern works: + + .. code-block:: bash + :caption: Example GNU Parallel command + + parallel --jobs 6 --colsep ' ' -k \ + './main3d.gnu.ex inputs param1={1} param2={2} param3={3} \ + amr.datalog=datalog_{#}.txt \ + amr.plot_file=plt_{#} \ + amr.check_file=chk_{#} \ + && tail -1 datalog_{#}.txt' \ + :::: ptrain.txt > ytrain.txt + + **Understanding the placeholders:** + + - ``{1}``, ``{2}``, ``{3}`` - **Column numbers** from the input file (``ptrain.txt``) + - ``{#}`` - **Line number** from the input file (used for unique file naming) + + **How it works:** + + 1. **Input file (ptrain.txt)** - Space/tab-separated parameter samples (one row per simulation): + + .. code-block:: text + + 1.0 1.0 0.01 + 0.75 1.25 0.0125 + 1.25 0.75 0.0075 + + 2. **For each row**, GNU Parallel: + + - Substitutes ``{1}`` with column 1 (e.g., ``1.0``) → ``param1=1.0`` + - Substitutes ``{2}`` with column 2 (e.g., ``1.0``) → ``param2=1.0`` + - Substitutes ``{3}`` with column 3 (e.g., ``0.01``) → ``param3=0.01`` + - Substitutes ``{#}`` with the line number (e.g., ``1``) → ``datalog_1.txt``, ``plt_1``, ``chk_1`` + + 3. **After simulation**, extracts output with ``tail -1 datalog_{#}.txt`` + + 4. **Output file (ytrain.txt)** - Collected results (one row per simulation): + + .. code-block:: text + + 1.5801622094 + 1.6234567890 + 1.4567890123 + + **Key options:** + + - ``--jobs 6`` - Run 6 simulations in parallel + - ``--colsep ' '`` - Columns separated by spaces (use ``'\t'`` for tabs) + - ``-k`` - Keep output order matching input order + - ``::::`` - Read input from file (``ptrain.txt``) + + **Output extraction alternatives:** + + Instead of ``tail -1 datalog_{#}.txt``, you can use other extraction methods (see :ref:`pytuq_extending_tutorial` for output examples): + + - **Plotfile tools**: ``fextrema.gnu.ex plt_{#} | tail -1 | awk '{print $3}'`` + - **Custom post-processing script**: ``./postprocess_run.sh {#}`` + + A post-processing script can take the line number ``{#}`` as an argument to locate and process the corresponding simulation outputs (datalog, plotfile, etc.), then print the space/tab-separated row for that simulation. + + This pattern makes it easy to: + + - Take a parameter file with N rows (one per simulation) + - Run simulations in parallel with unique output files + - Collect results into an output file with N rows (one per simulation result) + C++ AMReX + PyTUQ (python driven) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 5321b3378a8be576a1d2240a2f73d19519613029 Mon Sep 17 00:00:00 2001 From: "Jean M. Sexton" Date: Mon, 13 Oct 2025 13:01:27 -0700 Subject: [PATCH 111/142] Use more detailed install docs including conda --- Docs/source/HeatEquation_UQ.rst | 41 +++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index b29b6e24..bb7396e0 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -29,20 +29,51 @@ Located in ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ``, this example ana Installation ------------ +Install pytuq as described in `pytuq/README.md `_: + +.. note:: + + For NERSC users, consider placing your conda environment in ``/global/common/software`` + for better performance and persistence. Also, remember to ``module load conda`` before creating environments and installing. See the `NERSC Python documentation + `_ + for details. + .. code-block:: bash - :caption: Quick install + :caption: Pytuq installation script + + #!/bin/bash + + # For NERSC: module load conda + # 1. Clone repositories git clone --recursive --branch v1.0.0z https://github.com/sandialabs/pytuq + + # 2. Setup conda environment (optional, you can add to an existing env) + # Create conda environment (use -y for non-interactive) + conda create -y --name pytuq_integration python=3.11 --no-default-packages + + # For NERSC (see https://docs.nersc.gov/development/languages/python/nersc-python/#moving-your-conda-setup-to-globalcommonsoftware): + # conda create -y --prefix /global/common/software/myproject/$USER/pytuq_integration python=3.11 + + conda activate pytuq_integration + # For NERSC: conda activate /global/common/software/myproject/$USER/pytuq_integration + + # 3. Install PyTUQ cd pytuq - echo "dill" >> requirements.txt - pip install -r requirements.txt - pip install . + python -m pip install -r requirements.txt + python -m pip install . + conda install -y dill + cd ../ + + # 4. Verify installation + conda list | grep pytuq # Should show pytuq 1.0.0z .. note:: **Comprehensive Installation:** - For a detailed installation script that includes AMReX, pyAMReX, and PyTUQ setup in a conda environment, see ``GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh`` + For a detailed installation script that includes AMReX, pyAMReX, and PyTUQ setup in a conda environment, see ``GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh`` `example_detailed_install.sh <../../../GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh>`_ + Examples -------- From e1a7daa83bb73e9e2d79cc5d75ba454488d0bc3d Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Tue, 14 Oct 2025 07:53:36 -0700 Subject: [PATCH 112/142] inputs reduce number of steps and reduce dt so final solution looks more similar to initial solution (easier to debug) plot_int = -1 main.cpp change function to "init_amplitude * exp(-r^2)" instead of "1 + init_amplitude * exp(-r^2)" so we can visually see init_amplitude easier wf_uqpc.x remove unused output to pnames.txt and outnames.txt and put them in comments instead add comments bugfix: replace input qsam.txt with psam.txt --- GuidedTutorials/HeatEquation_UQ/Case-1/inputs | 12 ++++----- .../HeatEquation_UQ/Case-1/main.cpp | 2 +- .../HeatEquation_UQ/Case-1/wf_uqpc.x | 26 +++++++++---------- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/inputs b/GuidedTutorials/HeatEquation_UQ/Case-1/inputs index 56ec738e..71de8e37 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/inputs +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/inputs @@ -1,16 +1,16 @@ # Grid/Domain parameters n_cell = 32 # number of cells on each side of domain max_grid_size = 16 # size of each box (or grid) - + # Time stepping parameters -nsteps = 100 # total steps in simulation -dt = 1.e-5 # time step +nsteps = 10 # total steps in simulation +dt = 1.e-6 # time step # Output control -plot_int = 100 # how often to write plotfile (-1 = no plots) +plot_int = -1 # how often to write plotfile (-1 = no plots) datalog_int = -1 # how often to write datalog (-1 = no regular output) -# Physics parameters (these are what we vary for UQ) +# Physics parameters (these are what we vary for UQ and values here will be overwritten diffusion_coeff = 1.0 # diffusion coefficient for heat equation init_amplitude = 1.0 # amplitude of initial temperature profile -init_width = 0.01 # width parameter (variance, not std dev) \ No newline at end of file +init_width = 0.01 # width parameter (variance, not std dev) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp index 49786599..b5525d06 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp @@ -199,7 +199,7 @@ amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; // Set initial temperature profile - phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); + phiOld(i,j,k) = init_amplitude * std::exp(-rsquared); }); } diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x index b97cecd1..9610cf1e 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x @@ -1,35 +1,33 @@ #!/bin/bash -e -#SDIR=`dirname "$0"` +# location of pytuq export KLPC=$(pwd)/../../../../pytuq #First-order Polynomial Chaos (PC). ## Given mean and standard deviation of each normal random parameter +# inputs are diffusion_coeff, init_amplitude, init_width echo "1 0.25 " > param_margpc.txt echo "1 0.25" >> param_margpc.txt echo "0.01 0.0025" >> param_margpc.txt -echo "diffusion_coeff" > pnames.txt -echo "init_amplitude" >> pnames.txt -echo "init_width" >> pnames.txt - -echo "max_temp" > outnames.txt -echo "mean_temp" >> outnames.txt -echo "std_temp" >> outnames.txt -echo "total_energy" >> outnames.txt - PCTYPE="HG" ORDER=1 -NSAM=111 +NSAM=10 +# generate pcf.txt (re-ordering of parame_margpc.txt) ${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER + +# generate qsam.txt (random numbers drawn from polynomial chaos basis) +# generate qsam.txt (the randomly varying inputs parameters ${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM -parallel --jobs 6 --keep-order --colsep ' ' \ +# run all the jobs with psam.txt as the parameters, and log the final outputs in ysam.txt +# outputs to ysam.txt are max_temp, mean_temp, std_temp, total_energy +parallel --jobs 4 --keep-order --colsep ' ' \ './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ datalog=datalog_{#}.txt \ - plot_int = -1 > /dev/null 2>&1 \ + > /dev/null 2>&1 \ && tail -1 datalog_{#}.txt' \ - :::: qsam.txt > ysam.txt + :::: psam.txt > ysam.txt ${KLPC}/apps/pc_fit.py --pctype $PCTYPE --order $ORDER --xdata "qsam.txt" --ydata "ysam.txt" From 88def0b0f0b0b556e6b255703cfa7a72cd026851 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Tue, 14 Oct 2025 07:56:39 -0700 Subject: [PATCH 113/142] change NSAM (number of samples) to 20 so it runs faster --- GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x index 9610cf1e..039ba0b6 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x @@ -12,7 +12,7 @@ echo "0.01 0.0025" >> param_margpc.txt PCTYPE="HG" ORDER=1 -NSAM=10 +NSAM=20 # generate pcf.txt (re-ordering of parame_margpc.txt) ${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER From bf6e2a5387181b56d3f492e4b3026f5cc5f5b3f0 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Tue, 14 Oct 2025 08:07:27 -0700 Subject: [PATCH 114/142] update docs based on previous commits --- Docs/source/HeatEquation_UQ.rst | 57 +++++++++++++++++---------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index bb7396e0..8b64ad09 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -92,7 +92,7 @@ C++ AMReX + PyTUQ (BASH driven) .. code-block:: bash :caption: Build C++ example - cd GuidedTutorials/HeatEquation_UQ/Case-1 + cd amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-1 make -j4 .. code-block:: bash @@ -102,57 +102,60 @@ C++ AMReX + PyTUQ (BASH driven) .. dropdown:: Understanding GNU Parallel Workflow Pattern - After PyTUQ scripts generate an input parameter file (``ptrain.txt``), the Case-1 workflow uses GNU Parallel to run multiple simulations efficiently and collect outputs into a results file (``ytrain.txt``) that PyTUQ can use for surrogate model fitting. Here's how the pattern works: + First, this script generate + + After PyTUQ scripts generate an input parameter file (``psam.txt``), the Case-1 workflow uses GNU Parallel to run multiple simulations efficiently and collect outputs into a results file (``ysam.txt``) that PyTUQ can use for surrogate model fitting. Here's how the pattern works: .. code-block:: bash - :caption: Example GNU Parallel command + :caption: Example GNU Parallel command (``wf_uqpc.x``) - parallel --jobs 6 --colsep ' ' -k \ - './main3d.gnu.ex inputs param1={1} param2={2} param3={3} \ - amr.datalog=datalog_{#}.txt \ - amr.plot_file=plt_{#} \ - amr.check_file=chk_{#} \ - && tail -1 datalog_{#}.txt' \ - :::: ptrain.txt > ytrain.txt + parallel --jobs 4 --keep-order --colsep ' ' \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + datalog=datalog_{#}.txt \ + > /dev/null 2>&1 \ + && tail -1 datalog_{#}.txt' \ + :::: psam.txt > ysam.txt **Understanding the placeholders:** - - ``{1}``, ``{2}``, ``{3}`` - **Column numbers** from the input file (``ptrain.txt``) + - ``{1}``, ``{2}``, ``{3}`` - **Column numbers** from the input file (``psam.txt``) - ``{#}`` - **Line number** from the input file (used for unique file naming) **How it works:** - 1. **Input file (ptrain.txt)** - Space/tab-separated parameter samples (one row per simulation): + 1. **Input file (psam.txt)** - Space/tab-separated parameter samples (one row per simulation): .. code-block:: text - 1.0 1.0 0.01 - 0.75 1.25 0.0125 - 1.25 0.75 0.0075 + 1.252744433619e+00 1.189332992765e+00 1.107922825308e-02 + 1.153626977108e+00 8.912357108037e-01 8.456030290466e-03 + 3.265959848625e-01 7.285478940917e-01 7.687549687284e-03 2. **For each row**, GNU Parallel: - - Substitutes ``{1}`` with column 1 (e.g., ``1.0``) → ``param1=1.0`` - - Substitutes ``{2}`` with column 2 (e.g., ``1.0``) → ``param2=1.0`` - - Substitutes ``{3}`` with column 3 (e.g., ``0.01``) → ``param3=0.01`` - - Substitutes ``{#}`` with the line number (e.g., ``1``) → ``datalog_1.txt``, ``plt_1``, ``chk_1`` + - Substitutes ``{1}`` with column 1 + - Substitutes ``{2}`` with column 2 + - Substitutes ``{3}`` with column 3 + - Substitutes ``{#}`` with the line number (e.g., ``1``) → ``datalog_1.txt`` - 3. **After simulation**, extracts output with ``tail -1 datalog_{#}.txt`` + 3. **After simulation**, extracts output from final line of simulation standard output with ``tail -1 datalog_{#}.txt`` - 4. **Output file (ytrain.txt)** - Collected results (one row per simulation): + 4. **Output file (ysam.txt)** - Collected results (one row per simulation): .. code-block:: text - 1.5801622094 - 1.6234567890 - 1.4567890123 + 1.106358248808103 0.007723115686160915 0.0562709919848521 253.0710548041209 + 0.8113473575862051 0.003858932516788893 0.03451729840856418 126.4495007101384 + 0.6608518630828316 0.002734417468982227 0.02636512039600031 89.60139162360962 + 1.082407360836961 0.01225809407946649 0.06904148772504816 401.6732267959578 + 1.279158901768254 0.006549137494533053 0.05630882962684351 214.6021374208591 **Key options:** - - ``--jobs 6`` - Run 6 simulations in parallel + - ``--jobs 4`` - Run 4 simulations in parallel - ``--colsep ' '`` - Columns separated by spaces (use ``'\t'`` for tabs) - - ``-k`` - Keep output order matching input order - - ``::::`` - Read input from file (``ptrain.txt``) + - ``-k`` - Keep output order matching input order (important for parallel jobs) + - ``::::`` - Read input from file (``psam.txt``) **Output extraction alternatives:** From d6ef37bc4d8f22e588203be8297f3d9cb4cf1820 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Wed, 15 Oct 2025 17:38:45 -0700 Subject: [PATCH 115/142] tiny non-functional tweaks (except add the "1+" to the initial condition) to make this exactly match the HeatEquation_Simple/main.cpp except for the minimal changes (for easy tkdiff'ing) --- .../HeatEquation_UQ/Case-1/main.cpp | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp index b5525d06..a8026d7b 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp @@ -9,6 +9,7 @@ #include #include + int main (int argc, char* argv[]) { amrex::Initialize(argc,argv); @@ -53,7 +54,6 @@ int main (int argc, char* argv[]) const int datprecision = 16; const int timeprecision = 13; int datalog_int = -1; // Interval for regular output (<=0 means no regular output) - bool datalog_final = true; // Write datalog at final step std::string datalog_filename = "datalog.txt"; // ********************************** @@ -82,6 +82,9 @@ int main (int argc, char* argv[]) plot_int = -1; pp.query("plot_int",plot_int); + // time step + pp.get("dt",dt); + // Default datalog_int to -1, allow us to set it to something else in the inputs file // If datalog_int < 0 then no plot files will be written datalog_int = -1; @@ -90,9 +93,6 @@ int main (int argc, char* argv[]) datalog_filename = "datalog.txt"; pp.query("datalog",datalog_filename); - // time step - pp.get("dt",dt); - // ********************************** // READ PHYSICS PARAMETERS // ********************************** @@ -186,27 +186,25 @@ int main (int argc, char* argv[]) // - width: controls spread of initial hot spot // - smaller width = more concentrated // - larger width = more spread out - -amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) + amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) { - // Calculate physical coordinates of cell center + + // ********************************** + // SET VALUES FOR EACH CELL + // ********************************** + amrex::Real x = (i+0.5) * dx[0]; amrex::Real y = (j+0.5) * dx[1]; amrex::Real z = (k+0.5) * dx[2]; - - // Calculate squared distance from domain center (0.5, 0.5, 0.5) - // Divide by init_width (which is the variance, not standard deviation) - amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; - - // Set initial temperature profile - phiOld(i,j,k) = init_amplitude * std::exp(-rsquared); + amrex::Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/init_width; + phiOld(i,j,k) = 1. + init_amplitude * std::exp(-rsquared); }); } // ********************************** // WRITE DATALOG FILE // ********************************** - if (amrex::ParallelDescriptor::IOProcessor() && (datalog_int>0 || datalog_final)) { + if (amrex::ParallelDescriptor::IOProcessor()) { std::ofstream datalog(datalog_filename); // truncate mode to start fresh datalog << "#" << std::setw(datwidth-1) << " max_temp"; datalog << std::setw(datwidth) << " mean_temp"; @@ -247,17 +245,19 @@ amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) const amrex::Array4& phiOld = phi_old.array(mfi); const amrex::Array4& phiNew = phi_new.array(mfi); - // advance the data by dt using heat equation with diffusion coefficient + // advance the data by dt amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) { - // Calculate the discrete Laplacian using finite differences - amrex::Real laplacian = - (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) - +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) - +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]); - - // Apply heat equation using diffusion_coeff - matches Python version - phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; + + // ********************************** + // EVOLVE VALUES FOR EACH CELL + // ********************************** + + phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * + ( (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) + +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) + +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]) + ); }); } @@ -280,7 +280,7 @@ amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) // Check if we should write datalog bool write_datalog = false; - if (datalog_final && step == nsteps) { + if (step == nsteps) { write_datalog = true; // Write final step } else if (datalog_int > 0 && step % datalog_int == 0) { write_datalog = true; // Write every datalog_int steps @@ -320,6 +320,7 @@ amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) } } + } amrex::Finalize(); return 0; From 46479a2ee79464d26f0a756f9ba1227d7dee3499 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Wed, 15 Oct 2025 20:03:18 -0700 Subject: [PATCH 116/142] enhanced documentation --- Docs/source/HeatEquation_UQ.rst | 78 ++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 8b64ad09..6e207907 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -22,9 +22,21 @@ This tutorial demonstrates how to improve efficiency without sacrificing accurac PyTUQ (Python interface to the UQ Toolkit) provides specialized tools for surrogate construction and global sensitivity analysis, enabling rapid parameter space exploration and dimensionality reduction for scientific applications. -We demonstrate how to integrate PyTUQ with your AMReX application through three practical workflows: C++ executables managed with gnu parallel (Case-1), Python-driven C++ executables with bash run management (Case-2), and native PyAMReX applications (Case-3). +We demonstrate how to integrate PyTUQ with your AMReX application through three practical workflows: C++ executables managed with bash run management (Case-1), Python-driven C++ executables (Case-2), and native PyAMReX applications (Case-3). -Located in ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ``, this example analyzes a heat equation solver to illustrate the complete forward UQ workflow from parameter sampling through sensitivity analysis. After running the provided examples, the Customizing the Workflow section explains the underlying problem setup and provides step-by-step guidance for adapting this workflow to your own AMReX application +In these examples we model the heat equation + +.. math:: \frac{\partial\phi}{\partial t} = D\nabla^2 \phi, + +with initial condition + +.. math:: \phi_0 = 1 + A e^{-r^2 / W}, + +with uncertain parameters ``diffusion_coeff`` (:math:`D`), ``init_amplitude`` (:math:`A`), and ``init_width`` (:math:`W`). +The outputs of interest are the maximum temperature, mean temperature, standard deviation of temperature, +and total "energy" (i.e., summation of temperature over all grid points). + +Located in ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ``, these examples illustrate the complete forward UQ workflow from parameter sampling through sensitivity analysis. After running the provided examples, the Customizing the Workflow section explains the underlying problem setup and provides step-by-step guidance for adapting this workflow to your own AMReX application. Installation ------------ @@ -80,7 +92,7 @@ Examples C++ AMReX + PyTUQ (BASH driven) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - + .. dropdown:: Build and Run :open: @@ -102,13 +114,55 @@ C++ AMReX + PyTUQ (BASH driven) .. dropdown:: Understanding GNU Parallel Workflow Pattern - First, this script generate + The ``wf_uqpc.x`` bash script relies on the user augmenting their codes to write outputs of interest to ASCII text files. + In this case, the ``main.cpp`` was modified from the ``amrex-tutorials/GuidedTutorials/HeatEquation_Simple/main.cpp`` in the following ways: + + First, support for parsing ``diffusion_coeff``, ``init_amplitude``, and ``init_width`` from the input file and command line were added. - After PyTUQ scripts generate an input parameter file (``psam.txt``), the Case-1 workflow uses GNU Parallel to run multiple simulations efficiently and collect outputs into a results file (``ysam.txt``) that PyTUQ can use for surrogate model fitting. Here's how the pattern works: + Second, support for writing outputs of interest to ASCII text files is added. The ``datalog_filename`` input parameter is generated by the bash + script to give each simulation output a unique identifier. + The ``datalog_int`` parameter gives the user the option to write the outputs of interest at a given time step interval, but in this example, + the outputs of interest after the final step are those that matter, and are extracted by the bash script to create a master output file + containing a separate set of simulation outputs of interest in each row. + + The bash script calls PyTUQ scripts that generate an input parameter file (``psam.txt``) and then uses GNU Parallel to run multiple simulations + efficiently and collect outputs into a results file (``ysam.txt``) that PyTUQ can use for surrogate model fitting. + + Here's how the script works. + In the first part of the script, the user generates a ``param_margpc.txt`` file containing the mean and standard deviation + of each normal random parameter. Then pyTUQ scripts are called to generate the appropriate sampling based on polynomial chaos settings, + and ultimately creates ``psam.txt`` which contains the parameters (one set per line) for each simulation to be run. .. code-block:: bash :caption: Example GNU Parallel command (``wf_uqpc.x``) + #!/bin/bash -e + + # location of pytuq + export KLPC=$(pwd)/../../../../pytuq + + #First-order Polynomial Chaos (PC). + ## Given mean and standard deviation of each normal random parameter + # inputs are diffusion_coeff, init_amplitude, init_width + echo "1 0.25 " > param_margpc.txt + echo "1 0.25" >> param_margpc.txt + echo "0.01 0.0025" >> param_margpc.txt + + PCTYPE="HG" + ORDER=1 + NSAM=20 + + # generate pcf.txt (re-ordering of parame_margpc.txt) + ${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER + + # generate qsam.txt (random numbers drawn from polynomial chaos basis) and + # generate psam.txt (the randomly varying inputs parameters) + ${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM + + The next part of the script runs all of the simulation and collects the results. + + .. code-block:: bash + parallel --jobs 4 --keep-order --colsep ' ' \ './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ datalog=datalog_{#}.txt \ @@ -116,11 +170,6 @@ C++ AMReX + PyTUQ (BASH driven) && tail -1 datalog_{#}.txt' \ :::: psam.txt > ysam.txt - **Understanding the placeholders:** - - - ``{1}``, ``{2}``, ``{3}`` - **Column numbers** from the input file (``psam.txt``) - - ``{#}`` - **Line number** from the input file (used for unique file naming) - **How it works:** 1. **Input file (psam.txt)** - Space/tab-separated parameter samples (one row per simulation): @@ -172,6 +221,15 @@ C++ AMReX + PyTUQ (BASH driven) - Run simulations in parallel with unique output files - Collect results into an output file with N rows (one per simulation result) + Finally, the pyTUQ analyzes the results: + + .. code-block:: bash + + ${KLPC}/apps/pc_fit.py --pctype $PCTYPE --order $ORDER --xdata "qsam.txt" --ydata "ysam.txt" + +.. dropdown:: Understanding the Output + + C++ AMReX + PyTUQ (python driven) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 58347667801eacf1cf01b65bd0bf73605d4666fe Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Wed, 15 Oct 2025 20:05:53 -0700 Subject: [PATCH 117/142] fix typo in comment --- GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x index 039ba0b6..d8c65a3c 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x @@ -17,8 +17,8 @@ NSAM=20 # generate pcf.txt (re-ordering of parame_margpc.txt) ${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER -# generate qsam.txt (random numbers drawn from polynomial chaos basis) -# generate qsam.txt (the randomly varying inputs parameters +# generate qsam.txt (random numbers drawn from polynomial chaos basis) and +# generate psam.txt (the randomly varying inputs parameters) ${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM # run all the jobs with psam.txt as the parameters, and log the final outputs in ysam.txt From 1587bc1e30433cf51209ec14b00cb26496bcddfd Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Wed, 29 Oct 2025 09:13:38 -0700 Subject: [PATCH 118/142] pass in psam to pc_fit.py instead of qsam --- GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x index d8c65a3c..ee893ba0 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x @@ -30,4 +30,4 @@ parallel --jobs 4 --keep-order --colsep ' ' \ && tail -1 datalog_{#}.txt' \ :::: psam.txt > ysam.txt -${KLPC}/apps/pc_fit.py --pctype $PCTYPE --order $ORDER --xdata "qsam.txt" --ydata "ysam.txt" +${KLPC}/apps/pc_fit.py --pctype $PCTYPE --order $ORDER --xdata "psam.txt" --ydata "ysam.txt" From a6598436895a2cde0cc0efef4bd012fb231094d3 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Tue, 18 Nov 2025 13:48:39 -0800 Subject: [PATCH 119/142] update with newest main branch on pytuq repo --- GuidedTutorials/HeatEquation_UQ/Case-1/inputs | 6 +-- .../HeatEquation_UQ/Case-1/main.cpp | 21 ++++---- .../HeatEquation_UQ/Case-1/wf_uqpc_steps.x | 51 ------------------- 3 files changed, 13 insertions(+), 65 deletions(-) delete mode 100755 GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc_steps.x diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/inputs b/GuidedTutorials/HeatEquation_UQ/Case-1/inputs index 71de8e37..2758ec96 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/inputs +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/inputs @@ -4,13 +4,13 @@ max_grid_size = 16 # size of each box (or grid) # Time stepping parameters nsteps = 10 # total steps in simulation -dt = 1.e-6 # time step +dt = 5.e-7 # time step # Output control plot_int = -1 # how often to write plotfile (-1 = no plots) datalog_int = -1 # how often to write datalog (-1 = no regular output) # Physics parameters (these are what we vary for UQ and values here will be overwritten -diffusion_coeff = 1.0 # diffusion coefficient for heat equation -init_amplitude = 1.0 # amplitude of initial temperature profile +diffusion_coeff = 100. # diffusion coefficient for heat equation +init_amplitude = 1. # amplitude of initial temperature profile init_width = 0.01 # width parameter (variance, not std dev) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp index a8026d7b..d8a38de8 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp @@ -160,6 +160,7 @@ int main (int argc, char* argv[]) // we allocate two phi multifabs; one will store the old state, the other the new. amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); + amrex::MultiFab phi_tmp(ba, dm, Ncomp, Nghost); // time = starting time in the simulation amrex::Real time = 0.0; @@ -286,23 +287,21 @@ int main (int argc, char* argv[]) write_datalog = true; // Write every datalog_int steps } + amrex::MultiFab::Copy(phi_tmp, phi_new, 0, 0, 1, 0); + amrex::Real max_temperature = phi_new.max(0); + amrex::Real mean_temperature = phi_new.sum(0) / phi_new.boxArray().numPts(); + phi_tmp.plus(-mean_temperature,0,1,0); + amrex::Real std_temperature = phi_tmp.norm2(0); // compute sqrt( sum(phi_tmp_i^2) ); + amrex::Real integrated_temperature = phi_new.sum(0); + if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { std::ofstream datalog(datalog_filename, std::ios::app); - // Calculate temperature statistics - amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); - amrex::Real max_temperature = phi_new.max(0); - amrex::Real l2_norm = phi_new.norm2(0); - amrex::Real sum_sq = l2_norm * l2_norm; - amrex::Real variance = sum_sq / phi_new.boxArray().numPts() - mean_temp * mean_temp; - amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; - amrex::Real total_energy = phi_new.sum(0); - // Write 4 statistics datalog << std::setw(datwidth) << std::setprecision(datprecision) << max_temperature; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << mean_temp; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << mean_temperature; datalog << std::setw(datwidth) << std::setprecision(datprecision) << std_temperature; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << total_energy; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << integrated_temperature; datalog << std::endl; datalog.close(); diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc_steps.x b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc_steps.x deleted file mode 100755 index 7beffc19..00000000 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc_steps.x +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -e - -#SDIR=`dirname "$0"` -export KLPC=$(pwd)/../../../../pytuq - -#First-order Polynomial Chaos (PC). -## Given mean and standard deviation of each normal random parameter -echo "1 0.25 " > param_margpc.txt -echo "1 0.25" >> param_margpc.txt -echo "0.01 0.0025" >> param_margpc.txt - -echo "diffusion_coeff" > pnames.txt -echo "init_amplitude" >> pnames.txt -echo "init_width" >> pnames.txt - -echo "max_temp" > outnames.txt -echo "mean_temp" >> outnames.txt -echo "std_temp" >> outnames.txt -echo "total_energy" >> outnames.txt - -PCTYPE="HG" -ORDER=1 -NSAM=111 - -${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER -#${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM - -DIM=3 -NTRN=$NSAM -NTST=0 - -./uq_pc.py -r offline_prep -c pcf.txt -x ${PCTYPE} -d $DIM -o ${ORDER} -m lsq -s rand -n $NTRN -v $NTST - -cp ptrain.txt qsam.txt -#cp qsam.txt ptrain.txt - -parallel --jobs 6 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ - datalog=datalog_{#}.txt \ - plot_int = -1 > /dev/null 2>&1 \ - && tail -1 datalog_{#}.txt' \ - :::: qsam.txt > ysam.txt - -cp ysam.txt ytrain.txt - -#${KLPC}/apps/pc_fit.py --pctype $PCTYPE --order $ORDER --xdata "qsam.txt" --ydata "ysam.txt" -./uq_pc.py -r offline_post -c pcf.txt -x ${PCTYPE} -d $DIM -o ${ORDER} -m lsq -s rand -n $NTRN -v $NTST -t 5 - -./plot.py sens main - -./plot.py jsens From 591b70ac265b4bfa234c468ad2ff573affb8bb86 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Tue, 18 Nov 2025 14:23:05 -0800 Subject: [PATCH 120/142] updated documentation --- Docs/source/HeatEquation_UQ.rst | 131 ++++++++++++++++++++++---------- 1 file changed, 89 insertions(+), 42 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 6e207907..f3bf4753 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -1,3 +1,5 @@ +.. _tutorials_uq: + .. _guided_pytuq_integration: .. _pytuq_quickstart: @@ -58,27 +60,26 @@ Install pytuq as described in `pytuq/README.md param_margpc.txt + echo "100 25 " > param_margpc.txt echo "1 0.25" >> param_margpc.txt echo "0.01 0.0025" >> param_margpc.txt + PC_TYPE=HG # Hermite-Gaussian PC + INPC_ORDER=1 + # Creates input PC coefficient file pcf.txt (will have lots of zeros since we assume independent inputs) + ${PUQAPPS}/pc_prep.py -f marg -i param_margpc.txt -p ${INPC_ORDER} + + # Number of samples requested + NTRN=111 # Training + NTST=33 # Testing + + # Extract dimensionality d (i.e. number of input parameters) + DIM=`awk 'NR==1{print NF}' pcf.txt` - PCTYPE="HG" - ORDER=1 - NSAM=20 + # Output PC order + OUTPC_ORDER=3 - # generate pcf.txt (re-ordering of parame_margpc.txt) - ${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER + # Prepare inputs for the black-box model (use input PC to generate input samples for the model) + ${PUQAPPS}/pc_sam.py -f pcf.txt -t ${PC_TYPE} -n $NTRN + mv psam.txt ptrain.txt; mv qsam.txt qtrain.txt + ${PUQAPPS}/pc_sam.py -f pcf.txt -t ${PC_TYPE} -n $NTST + mv psam.txt ptest.txt; mv qsam.txt qtest.txt - # generate qsam.txt (random numbers drawn from polynomial chaos basis) and - # generate psam.txt (the randomly varying inputs parameters) - ${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM + # This creates files ptrain.txt, ptest.txt (train/test parameter inputs), qtrain.txt, qtest.txt (corresponding train/test stochastic PC inputs) + + # Optionally can provide pnames.txt and outnames.txt with input parameter names and output QoI names + # Or delete them to use generic names + rm -f pnames.txt outnames.txt The next part of the script runs all of the simulation and collects the results. .. code-block:: bash - parallel --jobs 4 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ - datalog=datalog_{#}.txt \ - > /dev/null 2>&1 \ - && tail -1 datalog_{#}.txt' \ - :::: psam.txt > ysam.txt + ################################ + ## 2. Run the black-box model ## + ################################ + + # Run the black-box model, can be any model from R^d to R^o) + # ptrain.txt is N x d input matrix, each row is a parameter vector of size d + # ytrain.txt is N x o output matrix, each row is a output vector of size o + + parallel --jobs 1 --keep-order --colsep ' ' \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + datalog=datalog_{#}.txt \ + > /dev/null 2>&1 \ + && tail -1 datalog_{#}.txt' \ + :::: ptrain.txt > ytrain.txt + + # Similar for testing + parallel --jobs 1 --keep-order --colsep ' ' \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + datalog=datalog_{#}.txt \ + > /dev/null 2>&1 \ + && tail -1 datalog_{#}.txt' \ + :::: ptest.txt > ytest.txt + + # This creates files ytrain.txt, ytest.txt (train/test model outputs) **How it works:** - 1. **Input file (psam.txt)** - Space/tab-separated parameter samples (one row per simulation): + 1. **Input file (ptrain.txt)** - Space/tab-separated parameter samples (one row per simulation): .. code-block:: text @@ -189,7 +231,7 @@ C++ AMReX + PyTUQ (BASH driven) 3. **After simulation**, extracts output from final line of simulation standard output with ``tail -1 datalog_{#}.txt`` - 4. **Output file (ysam.txt)** - Collected results (one row per simulation): + 4. **Output file (ytrain.txt)** - Collected results (one row per simulation): .. code-block:: text @@ -204,7 +246,7 @@ C++ AMReX + PyTUQ (BASH driven) - ``--jobs 4`` - Run 4 simulations in parallel - ``--colsep ' '`` - Columns separated by spaces (use ``'\t'`` for tabs) - ``-k`` - Keep output order matching input order (important for parallel jobs) - - ``::::`` - Read input from file (``psam.txt``) + - ``::::`` - Read input from file (``ptrain.txt``) **Output extraction alternatives:** @@ -225,7 +267,12 @@ C++ AMReX + PyTUQ (BASH driven) .. code-block:: bash - ${KLPC}/apps/pc_fit.py --pctype $PCTYPE --order $ORDER --xdata "qsam.txt" --ydata "ysam.txt" + ############################## + # 3. Build PC surrogate ## + ############################## + + # Build surrogate for each output (in other words, build output PC) + ${UQPC}/uq_pc.py -r offline -c pcf.txt -x ${PC_TYPE} -d $DIM -o ${INPC_ORDER} -m anl -s rand -n $NTRN -v $NTST -t ${OUTPC_ORDER} .. dropdown:: Understanding the Output From 361ee1ed767a6a120d1cde4e687249379a45c67e Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Wed, 19 Nov 2025 17:39:22 -0800 Subject: [PATCH 121/142] new workflow files --- .../HeatEquation_UQ/Case-1/workflow_uqpc.x | 208 ++++++++++++++++++ .../Case-1/{wf_uqpc.x => workflow_uqpc_js.x} | 0 2 files changed, 208 insertions(+) create mode 100755 GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x rename GuidedTutorials/HeatEquation_UQ/Case-1/{wf_uqpc.x => workflow_uqpc_js.x} (100%) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x new file mode 100755 index 00000000..ba2d37d2 --- /dev/null +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x @@ -0,0 +1,208 @@ +#!/bin/bash -e +#===================================================================================== + +# Script location +export UQPC=../../../../pytuq/apps/uqpc +export PUQAPPS=$UQPC/.. + + +################################ +## 0. Setup the problem ## +################################ + + +## Four simple options for uncertain input parameter setup. +## Uncomment one of them. + +## (a) Given mean and standard deviation of each normal random parameter +# inputs are diffusion_coeff, init_amplitude, init_width +echo "100 25 " > param_margpc.txt +echo "1 0.25" >> param_margpc.txt +echo "0.01 0.0025" >> param_margpc.txt +PC_TYPE=HG # Hermite-Gaussian PC +INPC_ORDER=1 +# Creates input PC coefficient file pcf.txt (will have lots of zeros since we assume independent inputs) +${PUQAPPS}/pc_prep.py -f marg -i param_margpc.txt -p ${INPC_ORDER} + +# ## (b) Given mean and half-width of each uniform random parameter +# echo "1 0.3 " > param_margpc.txt +# echo "3 0.1" >> param_margpc.txt +# echo "1 0.5" >> param_margpc.txt +# PC_TYPE=LU # Legendre-Uniform PC +# INPC_ORDER=1 +# # Creates input PC coefficient file pcf.txt (will have lots of zeros since we assume independent inputs) +# ${PUQAPPS}/pc_prep.py -f marg -i param_margpc.txt -p ${INPC_ORDER} + +# ## (c) Given mean and covariance of multivariate normal random parameter +# echo "3.0" > mean.txt +# echo "-2.1" >> mean.txt +# echo "1.5 0.4" > cov.txt +# echo "0.4 2.0" >> cov.txt +# PC_TYPE=HG # Hermite-Gaussian PC +# INPC_ORDER=1 +# # Creates input PC coefficient file pcf.txt +# ${PUQAPPS}/pc_prep.py -f mvn -i mean.txt -c cov.txt +# # Visualize covariance +# ${PUQAPPS}/plot_cov.py -m mean.txt -c cov.txt + +# ## (d) Given samples of inputs in psam.txt (e.g. from a prior calibration study) +# ${PUQAPPS}/pc_prep.py -f sam -i psam.txt -p ${INPC_ORDER} +# PC_TYPE=HG # Hermite-Gaussian PC +# INPC_ORDER=3 + +# Number of samples requested +NTRN=111 # Training +NTST=33 # Testing + +# Extract dimensionality d (i.e. number of input parameters) +DIM=`awk 'NR==1{print NF}' pcf.txt` + +# Output PC order +OUTPC_ORDER=3 + +#################################### +## 1-2. Online UQ with model.x ## +#################################### +## Run UQ with model.x in an online fashion (i.e. this is equivalent to the steps 1-2 below) +# Can uncomment this and comment out steps 1-2 below. +# ${UQPC}/uq_pc.py -r online_bb -c pcf.txt -x ${PC_TYPE} -d $DIM -o ${INPC_ORDER} -m anl -s rand -n $NTRN -v $NTST -t ${OUTPC_ORDER} + + +#################################### +## 1-2. Pre-run input/output ## +#################################### + +# # Quick and dirty equivalent if user already has the input/output pairs +# # Can uncomment this and comment out steps 1-2 below, IF input.txt and output.txt are available +# ln -sf input.txt pall.txt +# ln -sf output.txt yall.txt +# # Get ranges of inputs, with 10% 'cushion' from the dimension-wise extreme samples +# ${UQPC}/../awkies/getrange.x pall.txt 0.1 > param_range.txt +# # Scale the inputs +# ${UQPC}/../awkies/scale.x pall.txt from param_range.txt qall.txt + +# # This is not ideal if input/output have strictly fewer or more rows than NTRN+NTST +# head -n$NTRN pall.txt > ptrain.txt +# head -n$NTRN qall.txt > qtrain.txt +# head -n$NTRN yall.txt > ytrain.txt + +# tail -n$NTST pall.txt > ptest.txt +# tail -n$NTST qall.txt > qtest.txt +# tail -n$NTST yall.txt > ytest.txt + +################################### +## 1-3. Online UQ with model() ## +################################### +## Run UQ with model() function defined in uq_pc.py (i.e. this is equivalent to the steps 1-3 below) +# Can uncomment this and comment out steps 1-3 below. +# ${UQPC}/uq_pc.py -r online_example -c pcf.txt -x ${PC_TYPE} -d $DIM -o ${INPC_ORDER} -m anl -s rand -n $NTRN -v $NTST -t ${OUTPC_ORDER} + + + +############################### +## 1. Prepare the inputs ## +############################### + +# Prepare inputs for the black-box model (use input PC to generate input samples for the model) +${PUQAPPS}/pc_sam.py -f pcf.txt -t ${PC_TYPE} -n $NTRN +mv psam.txt ptrain.txt; mv qsam.txt qtrain.txt +${PUQAPPS}/pc_sam.py -f pcf.txt -t ${PC_TYPE} -n $NTST +mv psam.txt ptest.txt; mv qsam.txt qtest.txt + + +# This creates files ptrain.txt, ptest.txt (train/test parameter inputs), qtrain.txt, qtest.txt (corresponding train/test stochastic PC inputs) + + +# Optionally can provide pnames.txt and outnames.txt with input parameter names and output QoI names +# Or delete them to use generic names +rm -f pnames.txt outnames.txt + +################################ +## 2. Run the black-box model ## +################################ + +# Run the black-box model, can be any model from R^d to R^o) +# ptrain.txt is N x d input matrix, each row is a parameter vector of size d +# ytrain.txt is N x o output matrix, each row is a output vector of size o + +parallel --jobs 1 --keep-order --colsep ' ' \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + datalog=datalog_{#}.txt \ + > /dev/null 2>&1 \ + && tail -1 datalog_{#}.txt' \ + :::: ptrain.txt > ytrain.txt + +# Similar for testing +parallel --jobs 1 --keep-order --colsep ' ' \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + datalog=datalog_{#}.txt \ + > /dev/null 2>&1 \ + && tail -1 datalog_{#}.txt' \ + :::: ptest.txt > ytest.txt + +# This creates files ytrain.txt, ytest.txt (train/test model outputs) + +############################## +# 3. Build PC surrogate ## +############################## + +# Build surrogate for each output (in other words, build output PC) +${UQPC}/uq_pc.py -r offline -c pcf.txt -x ${PC_TYPE} -d $DIM -o ${INPC_ORDER} -m anl -s rand -n $NTRN -v $NTST -t ${OUTPC_ORDER} + +# This creates files results.pk (Python pickle file encapsulating the results) + + +# Quick equivalent but here results are saved differently +# ${PUQAPPS}/pc_fit.py -x qtrain.txt -y ytrain.txt -c ${PC_TYPE} -o ${OUTPC_ORDER} +# This creates files pcrv.pk and lregs.pk (Python pickle file encapsulating the results) + + +################################### +## 4. Visualize the i/o data ## +################################### + +awk '{print "Training"}' ytrain.txt > labels.txt +awk '{print "Testing"}' ytest.txt >> labels.txt +cat ytrain.txt ytest.txt > yall.txt +cat ptrain.txt ptest.txt > pall.txt + +${PUQAPPS}/plot_xx.py -x pall.txt -l labels.txt +${PUQAPPS}/plot_pcoord.py -x pall.txt -y yall.txt -l labels.txt + +${PUQAPPS}/plot_yx.py -x ptrain.txt -y ytrain.txt -c 3 -r 1 +${PUQAPPS}/plot_yxx.py -x ptrain.txt -y ytrain.txt +${PUQAPPS}/plot_pdfs.py -p ptrain.txt; cp pdf_tri.png pdf_tri_inputs.png +${PUQAPPS}/plot_pdfs.py -p ytrain.txt; cp pdf_tri.png pdf_tri_outputs.png +${PUQAPPS}/plot_ens.py -y ytrain.txt + +# A lot of .png files are created for visualizing the input/output data + +################################ +## 5. Postprocess the results ## +################################ + + +# Plot model vs PC for each output +${UQPC}/plot.py dm training testing +# Plot model vs PC for each sample +${UQPC}/plot.py fit training testing + +# Plot output pdfs +${UQPC}/plot.py pdf +# Plot output pdfs in joyplot format +${UQPC}/plot.py joy + +# Plot main Sobol sensitivities (barplot) +${UQPC}/plot.py sens main +# Plot joint Sobol sensitivities (circular plot) +${UQPC}/plot.py jsens +# Plot matrix of Sobol sensitivities (rectangular plot) +${UQPC}/plot.py sensmat main + +# Plot 1d slices of the PC surrogate +${UQPC}/plot.py 1d training testing +# Plot 2d slices of the PC surrogate +${UQPC}/plot.py 2d + +# This creates .png files for visualizing PC surrogate results and sensitivity analysis + diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc_js.x similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.x rename to GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc_js.x From 1b9bdee5e38df241e2a80a525df03a7ebf34863e Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Mon, 24 Nov 2025 12:49:27 -0800 Subject: [PATCH 122/142] change integrate temperature to cell value at (9,9,9) to avoid singularity in plot add pnames.txt and outnames.txt to workflow_uqpc.x --- .../HeatEquation_UQ/Case-1/main.cpp | 121 +++++++++++------- .../HeatEquation_UQ/Case-1/workflow_uqpc.x | 13 +- 2 files changed, 85 insertions(+), 49 deletions(-) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp index d8a38de8..8a79a2e4 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp @@ -9,6 +9,7 @@ #include #include +using namespace amrex; int main (int argc, char* argv[]) { @@ -32,20 +33,20 @@ int main (int argc, char* argv[]) int plot_int; // time step - amrex::Real dt; + Real dt; // ********************************** // DECLARE PHYSICS PARAMETERS // ********************************** // diffusion coefficient for heat equation - amrex::Real diffusion_coeff; + Real diffusion_coeff; // amplitude of initial temperature profile - amrex::Real init_amplitude; + Real init_amplitude; // width parameter controlling spread of initial profile (variance, not std dev) - amrex::Real init_width; + Real init_width; // ********************************** // DECLARE DATALOG PARAMETERS @@ -64,7 +65,7 @@ int main (int argc, char* argv[]) // ParmParse is way of reading inputs from the inputs file // pp.get means we require the inputs file to have it // pp.query means we optionally need the inputs file to have it - but we must supply a default here - amrex::ParmParse pp; + ParmParse pp; // We need to get n_cell from the inputs file - this is the number of cells on each side of // a square (or cubic) domain. @@ -119,15 +120,15 @@ int main (int argc, char* argv[]) // ba will contain a list of boxes that cover the domain // geom contains information such as the physical domain size, // number of points in the domain, and periodicity - amrex::BoxArray ba; - amrex::Geometry geom; + BoxArray ba; + Geometry geom; // define lower and upper indices - amrex::IntVect dom_lo(0,0,0); - amrex::IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); + IntVect dom_lo(0,0,0); + IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); // Make a single box that is the entire domain - amrex::Box domain(dom_lo, dom_hi); + Box domain(dom_lo, dom_hi); // Initialize the boxarray "ba" from the single box "domain" ba.define(domain); @@ -136,17 +137,17 @@ int main (int argc, char* argv[]) ba.maxSize(max_grid_size); // This defines the physical box, [0,1] in each direction. - amrex::RealBox real_box({ 0., 0., 0.}, + RealBox real_box({ 0., 0., 0.}, { 1., 1., 1.}); // periodic in all direction - amrex::Array is_periodic{1,1,1}; + Array is_periodic{1,1,1}; // This defines a Geometry object - geom.define(domain, real_box, amrex::CoordSys::cartesian, is_periodic); + geom.define(domain, real_box, CoordSys::cartesian, is_periodic); // extract dx from the geometry object - amrex::GpuArray dx = geom.CellSizeArray(); + GpuArray dx = geom.CellSizeArray(); // Nghost = number of ghost cells for each array int Nghost = 1; @@ -155,26 +156,26 @@ int main (int argc, char* argv[]) int Ncomp = 1; // How Boxes are distrubuted among MPI processes - amrex::DistributionMapping dm(ba); + DistributionMapping dm(ba); // we allocate two phi multifabs; one will store the old state, the other the new. - amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); - amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); - amrex::MultiFab phi_tmp(ba, dm, Ncomp, Nghost); + MultiFab phi_old(ba, dm, Ncomp, Nghost); + MultiFab phi_new(ba, dm, Ncomp, Nghost); + MultiFab phi_tmp(ba, dm, Ncomp, Nghost); // time = starting time in the simulation - amrex::Real time = 0.0; + Real time = 0.0; // ********************************** // INITIALIZE DATA LOOP // ********************************** // loop over boxes - for (amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi) + for (MFIter mfi(phi_old); mfi.isValid(); ++mfi) { - const amrex::Box& bx = mfi.validbox(); + const Box& bx = mfi.validbox(); - const amrex::Array4& phiOld = phi_old.array(mfi); + const Array4& phiOld = phi_old.array(mfi); // ********************************** // SET INITIAL TEMPERATURE PROFILE @@ -187,17 +188,17 @@ int main (int argc, char* argv[]) // - width: controls spread of initial hot spot // - smaller width = more concentrated // - larger width = more spread out - amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) + ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) { // ********************************** // SET VALUES FOR EACH CELL // ********************************** - amrex::Real x = (i+0.5) * dx[0]; - amrex::Real y = (j+0.5) * dx[1]; - amrex::Real z = (k+0.5) * dx[2]; - amrex::Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/init_width; + Real x = (i+0.5) * dx[0]; + Real y = (j+0.5) * dx[1]; + Real z = (k+0.5) * dx[2]; + Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/init_width; phiOld(i,j,k) = 1. + init_amplitude * std::exp(-rsquared); }); } @@ -205,12 +206,12 @@ int main (int argc, char* argv[]) // ********************************** // WRITE DATALOG FILE // ********************************** - if (amrex::ParallelDescriptor::IOProcessor()) { + if (ParallelDescriptor::IOProcessor()) { std::ofstream datalog(datalog_filename); // truncate mode to start fresh datalog << "#" << std::setw(datwidth-1) << " max_temp"; datalog << std::setw(datwidth) << " mean_temp"; datalog << std::setw(datwidth) << " std_temp"; - datalog << std::setw(datwidth) << " total_energy"; + datalog << std::setw(datwidth) << " cell_temp"; datalog << std::endl; datalog.close(); } @@ -223,7 +224,7 @@ int main (int argc, char* argv[]) if (plot_int > 0) { int step = 0; - const std::string& pltfile = amrex::Concatenate("plt",step,5); + const std::string& pltfile = Concatenate("plt",step,5); WriteSingleLevelPlotfile(pltfile, phi_old, {"phi"}, geom, time, 0); } @@ -239,15 +240,15 @@ int main (int argc, char* argv[]) // new_phi = old_phi + dt * Laplacian(old_phi) // loop over boxes - for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ) + for ( MFIter mfi(phi_old); mfi.isValid(); ++mfi ) { - const amrex::Box& bx = mfi.validbox(); + const Box& bx = mfi.validbox(); - const amrex::Array4& phiOld = phi_old.array(mfi); - const amrex::Array4& phiNew = phi_new.array(mfi); + const Array4& phiOld = phi_old.array(mfi); + const Array4& phiNew = phi_new.array(mfi); // advance the data by dt - amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) + ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) { // ********************************** @@ -262,6 +263,33 @@ int main (int argc, char* argv[]) }); } + // find the value in cell (9,9,9) + ReduceOps reduce_op; + ReduceData reduce_data(reduce_op); + using ReduceTuple = typename decltype(reduce_data)::Type; + + for ( MFIter mfi(phi_old); mfi.isValid(); ++mfi ) + { + const Box& bx = mfi.validbox(); + + const Array4& phiOld = phi_old.array(mfi); + const Array4& phiNew = phi_new.array(mfi); + + // advance the data by dt + reduce_op.eval(bx, reduce_data, + [=] AMREX_GPU_DEVICE (int i, int j, int k) -> ReduceTuple + { + if (i==9 && j==9 && k==9) { + return{phiNew(i,j,k)}; + } else { + return {0.}; + } + }); + } + + Real cell_temperature = get<0>(reduce_data.value()); + ParallelDescriptor::ReduceRealSum(cell_temperature); + // ********************************** // INCREMENT // ********************************** @@ -270,10 +298,10 @@ int main (int argc, char* argv[]) time = time + dt; // copy new solution into old solution - amrex::MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); + MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); // Tell the I/O Processor to write out which step we're doing - amrex::Print() << "Advanced step " << step << "\n"; + Print() << "Advanced step " << step << "\n"; // ********************************** // WRITE DATALOG AT GIVEN INTERVAL @@ -287,21 +315,20 @@ int main (int argc, char* argv[]) write_datalog = true; // Write every datalog_int steps } - amrex::MultiFab::Copy(phi_tmp, phi_new, 0, 0, 1, 0); - amrex::Real max_temperature = phi_new.max(0); - amrex::Real mean_temperature = phi_new.sum(0) / phi_new.boxArray().numPts(); + MultiFab::Copy(phi_tmp, phi_new, 0, 0, 1, 0); + Real max_temperature = phi_new.max(0); + Real mean_temperature = phi_new.sum(0) / phi_new.boxArray().numPts(); phi_tmp.plus(-mean_temperature,0,1,0); - amrex::Real std_temperature = phi_tmp.norm2(0); // compute sqrt( sum(phi_tmp_i^2) ); - amrex::Real integrated_temperature = phi_new.sum(0); + Real std_temperature = phi_tmp.norm2(0); // compute sqrt( sum(phi_tmp_i^2) ); - if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { + if (write_datalog && ParallelDescriptor::IOProcessor()) { std::ofstream datalog(datalog_filename, std::ios::app); // Write 4 statistics datalog << std::setw(datwidth) << std::setprecision(datprecision) << max_temperature; datalog << std::setw(datwidth) << std::setprecision(datprecision) << mean_temperature; datalog << std::setw(datwidth) << std::setprecision(datprecision) << std_temperature; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << integrated_temperature; + datalog << std::setw(datwidth) << std::setprecision(datprecision) << cell_temperature; datalog << std::endl; datalog.close(); @@ -314,15 +341,13 @@ int main (int argc, char* argv[]) // Write a plotfile of the current data (plot_int was defined in the inputs file) if (plot_int > 0 && step%plot_int == 0) { - const std::string& pltfile = amrex::Concatenate("plt",step,5); + const std::string& pltfile = Concatenate("plt",step,5); WriteSingleLevelPlotfile(pltfile, phi_new, {"phi"}, geom, time, step); } } } - amrex::Finalize(); + Finalize(); return 0; } - - diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x index ba2d37d2..2b5dcb9a 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x @@ -115,7 +115,18 @@ mv psam.txt ptest.txt; mv qsam.txt qtest.txt # Optionally can provide pnames.txt and outnames.txt with input parameter names and output QoI names # Or delete them to use generic names -rm -f pnames.txt outnames.txt +#rm -f pnames.txt outnames.txt + +echo "diffusion_coef" > pnames.txt +echo "init_amplitude" >> pnames.txt +echo "init_width" >> pnames.txt + +echo "max_temp" > outnames.txt +echo "mean_temp" >> outnames.txt +echo "std_temp" >> outnames.txt +echo "cell_temp" >> outnames.txt + + ################################ ## 2. Run the black-box model ## From 185fafd9033bf37e3315bff3a6423b9a7d7ebe1a Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Fri, 9 Jan 2026 13:28:02 -0800 Subject: [PATCH 123/142] amrex-tutorials docs sphinx build instructions --- Docs/Readme.sphinx | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 Docs/Readme.sphinx diff --git a/Docs/Readme.sphinx b/Docs/Readme.sphinx new file mode 100644 index 00000000..7ed0e5f4 --- /dev/null +++ b/Docs/Readme.sphinx @@ -0,0 +1,51 @@ +To update the online AMReX-Tutorials documentation using sphinx: + + 1. Checkout the main branch of amrex-tutorials + + 2. Go to amrex-tutorials/Docs/source and edit the relevant .rst file(s). + + 3. Commit/push your changes. + +The GitHub action script will build the html pages and commit them to AMReX-Tutorials' gh-pages repo for you. If you would like to +preview the html locally, you can: + + 4. Go back into amrex-tutorials/Docs. + + 5. Type "make html". This will build new html files in amrex-tutorials/Docs/build/html + +********************************************************************************** + +If you would like to build the web pages locally, you will need Python, the Sphinx software, and the "Read the Docs" theme. + +If you have conda, you can install the necessary packages as follows: + + 1. Type "conda install sphinx" + + 2. Type "conda install sphinx_rtd_theme" + +If you don't have a conda Python installation, you get one by doing the following: + + 1. Go to https://conda.io/miniconda.html and download the Python 3.6 64-bit (bash installer), "Miniconda3-latest-Linux-x86_64.sh". Save this script to your hard drive. + + 2. Type "bash Miniconda3-latest-Linux-x86_64.sh" and follow the installation prompts. + + 3. The install script will prompt you to add some commands to your .bashrc that add the miniconda install location to your PATH environment variable. + If Bash is your default shell, choose "yes". Otherwise, you will need to manually do the same for your shell of choice. + + 4. Either open a new terminal, or re-source the configuration file you just added to. E.g., for bash: + source ~/.bashrc + +You should now be able to successfully "make html" in amrex-tutorials/Docs. + +If you would like to make a pdf document from the *rst files, first + +sudo apt-get install latexmk +(if you are using macOS, latexmk is installed via the TexLive Utility) + +Then in the amrex-tutorials/Docs directory, type + +make latexpdf +(if you have the slimmed-down latex install, then you need to also install the following packages via TexLive Utility: tabulary, varwidth, framed, wrapfig, capt-of, needspace, courier) + + +This will create amrex-tutorials/Docs/build/latex/amrex.pdf From 30c7ee3b384192869bbbe83d453abca8920a5079 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Fri, 9 Jan 2026 13:34:19 -0800 Subject: [PATCH 124/142] style --- Docs/source/HeatEquation_UQ.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index f3bf4753..68369f54 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -31,7 +31,7 @@ In these examples we model the heat equation .. math:: \frac{\partial\phi}{\partial t} = D\nabla^2 \phi, with initial condition - + .. math:: \phi_0 = 1 + A e^{-r^2 / W}, with uncertain parameters ``diffusion_coeff`` (:math:`D`), ``init_amplitude`` (:math:`A`), and ``init_width`` (:math:`W`). @@ -93,7 +93,7 @@ Examples C++ AMReX + PyTUQ (BASH driven) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - + .. dropdown:: Build and Run :open: @@ -148,8 +148,8 @@ C++ AMReX + PyTUQ (BASH driven) ## 0. Setup the problem ## ################################ - ## Four simple options for uncertain input parameter setup. - ## Uncomment one of them. + ## Four simple options for uncertain input parameter setup. + ## Uncomment one of them. ## (a) Given mean and standard deviation of each normal random parameter # inputs are diffusion_coeff, init_amplitude, init_width @@ -163,7 +163,7 @@ C++ AMReX + PyTUQ (BASH driven) # Number of samples requested NTRN=111 # Training - NTST=33 # Testing + NTST=33 # Testing # Extract dimensionality d (i.e. number of input parameters) DIM=`awk 'NR==1{print NF}' pcf.txt` @@ -184,7 +184,7 @@ C++ AMReX + PyTUQ (BASH driven) rm -f pnames.txt outnames.txt The next part of the script runs all of the simulation and collects the results. - + .. code-block:: bash ################################ @@ -193,7 +193,7 @@ C++ AMReX + PyTUQ (BASH driven) # Run the black-box model, can be any model from R^d to R^o) # ptrain.txt is N x d input matrix, each row is a parameter vector of size d - # ytrain.txt is N x o output matrix, each row is a output vector of size o + # ytrain.txt is N x o output matrix, each row is a output vector of size o parallel --jobs 1 --keep-order --colsep ' ' \ './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ @@ -264,16 +264,16 @@ C++ AMReX + PyTUQ (BASH driven) - Collect results into an output file with N rows (one per simulation result) Finally, the pyTUQ analyzes the results: - + .. code-block:: bash - + ############################## # 3. Build PC surrogate ## ############################## # Build surrogate for each output (in other words, build output PC) ${UQPC}/uq_pc.py -r offline -c pcf.txt -x ${PC_TYPE} -d $DIM -o ${INPC_ORDER} -m anl -s rand -n $NTRN -v $NTST -t ${OUTPC_ORDER} - + .. dropdown:: Understanding the Output From de512e947427a8f4248d825c01e90317a41b4292 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Fri, 9 Jan 2026 13:38:57 -0800 Subject: [PATCH 125/142] additional conda/sphinx packages --- Docs/Readme.sphinx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Docs/Readme.sphinx b/Docs/Readme.sphinx index 7ed0e5f4..34fda8fa 100644 --- a/Docs/Readme.sphinx +++ b/Docs/Readme.sphinx @@ -23,6 +23,10 @@ If you have conda, you can install the necessary packages as follows: 2. Type "conda install sphinx_rtd_theme" + 3. Type "sphinx-design" + + 4. Type "conda install -c conda-forge sphinx-copybutton" + If you don't have a conda Python installation, you get one by doing the following: 1. Go to https://conda.io/miniconda.html and download the Python 3.6 64-bit (bash installer), "Miniconda3-latest-Linux-x86_64.sh". Save this script to your hard drive. From a0f79b6130ddcc5e84a29e0839cd358e65fc9cc3 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Fri, 9 Jan 2026 16:44:33 -0800 Subject: [PATCH 126/142] terminology: width -> variance and put a "2" in the e^-(r^2/(2*V)) to make it a real Gaussian --- Docs/source/HeatEquation_UQ.rst | 12 +++++------ .../HeatEquation_UQ_ExtendingTutorial.rst | 20 +++++++++---------- .../HeatEquation_UQ_MathematicalDetails.rst | 8 ++++---- GuidedTutorials/HeatEquation_UQ/Case-1/inputs | 2 +- .../HeatEquation_UQ/Case-1/main.cpp | 20 +++++++++---------- .../HeatEquation_UQ/Case-1/wf_uqpc.slurm | 4 ++-- .../HeatEquation_UQ/Case-1/workflow_uqpc.x | 18 +++++++++++------ .../HeatEquation_UQ/Case-1/workflow_uqpc_js.x | 4 ++-- 8 files changed, 47 insertions(+), 41 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 68369f54..9763ca24 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -32,9 +32,9 @@ In these examples we model the heat equation with initial condition -.. math:: \phi_0 = 1 + A e^{-r^2 / W}, +.. math:: \phi_0 = 1 + A e^{-r^2 / (2V)}, -with uncertain parameters ``diffusion_coeff`` (:math:`D`), ``init_amplitude`` (:math:`A`), and ``init_width`` (:math:`W`). +with uncertain parameters ``diffusion_coeff`` (:math:`D`), ``init_amplitude`` (:math:`A`), and ``init_variance`` (:math:`V`). The outputs of interest are the maximum temperature, mean temperature, standard deviation of temperature, and total "energy" (i.e., summation of temperature over all grid points). @@ -118,7 +118,7 @@ C++ AMReX + PyTUQ (BASH driven) The ``workflow_uqpc.x`` bash script relies on the user augmenting their codes to write outputs of interest to ASCII text files. In this case, the ``main.cpp`` was modified from the ``amrex-tutorials/GuidedTutorials/HeatEquation_Simple/main.cpp`` in the following ways: - First, support for parsing ``diffusion_coeff``, ``init_amplitude``, and ``init_width`` from the input file and command line were added. + First, support for parsing ``diffusion_coeff``, ``init_amplitude``, and ``init_variance`` from the input file and command line were added. Second, support for writing outputs of interest to ASCII text files is added. The ``datalog_filename`` input parameter is generated by the bash script to give each simulation output a unique identifier. @@ -152,7 +152,7 @@ C++ AMReX + PyTUQ (BASH driven) ## Uncomment one of them. ## (a) Given mean and standard deviation of each normal random parameter - # inputs are diffusion_coeff, init_amplitude, init_width + # inputs are diffusion_coeff, init_amplitude, init_variance echo "100 25 " > param_margpc.txt echo "1 0.25" >> param_margpc.txt echo "0.01 0.0025" >> param_margpc.txt @@ -196,7 +196,7 @@ C++ AMReX + PyTUQ (BASH driven) # ytrain.txt is N x o output matrix, each row is a output vector of size o parallel --jobs 1 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ datalog=datalog_{#}.txt \ > /dev/null 2>&1 \ && tail -1 datalog_{#}.txt' \ @@ -204,7 +204,7 @@ C++ AMReX + PyTUQ (BASH driven) # Similar for testing parallel --jobs 1 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ datalog=datalog_{#}.txt \ > /dev/null 2>&1 \ && tail -1 datalog_{#}.txt' \ diff --git a/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst b/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst index 4cf8c22a..574b750e 100644 --- a/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst +++ b/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst @@ -16,7 +16,7 @@ Identify / Add input parameters amrex::Real diffusion_coeff; amrex::Real init_amplitude; - amrex::Real init_width; + amrex::Real init_variance; 2. Read parameters from inputs file: @@ -28,15 +28,15 @@ Identify / Add input parameters init_amplitude = 1.0; pp.query("init_amplitude", init_amplitude); - init_width = 0.01; - pp.query("init_width", init_width); + init_variance = 0.01; + pp.query("init_variance", init_variance); 3. Use parameters in initial conditions and evolution: .. code-block:: cpp // Initial conditions - amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; + amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / (2.*init_variance); phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); // Evolution @@ -235,7 +235,7 @@ Add UQ parameters as function arguments: def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, plot_int: int = 100, dt: float = 1e-5, plot_files_output: bool = False, verbose: int = 1, diffusion_coeff: float = 1.0, init_amplitude: float = 1.0, - init_width: float = 0.01) -> Tuple[amr.MultiFab, amr.Geometry]: + init_variance: float = 0.01) -> Tuple[amr.MultiFab, amr.Geometry]: **2. Use parameters in physics** @@ -254,7 +254,7 @@ Replace hardcoded values with function parameters: rsquared = ((x[:, xp.newaxis, xp.newaxis] - 0.5)**2 + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 - + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / init_width + + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / init_variance phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + init_amplitude * xp.exp(-rsquared) .. code-block:: python @@ -293,7 +293,7 @@ Create a ``HeatEquationModel.py`` that inherits from ``AMReXBaseModel``: _param_fields = [ ('param', 'diffusion_coeff'), ('param', 'init_amplitude'), - ('param', 'init_width'), + ('param', 'init_variance'), ] _output_fields = [ @@ -309,7 +309,7 @@ Create a ``HeatEquationModel.py`` that inherits from ``AMReXBaseModel``: phi_new, geom = main( diffusion_coeff=float(param_set[0]), init_amplitude=float(param_set[1]), - init_width=float(param_set[2]), + init_variance=float(param_set[2]), plot_files_output=False, verbose=0 ) @@ -340,7 +340,7 @@ Specify mean and standard deviation for each uncertain parameter (one per line): echo "1 0.25 " > param_margpc.txt # diffusion_coeff: mean=1.0, std=0.25 echo "1 0.25" >> param_margpc.txt # init_amplitude: mean=1.0, std=0.25 - echo "0.01 0.0025" >> param_margpc.txt # init_width: mean=0.01, std=0.0025 + echo "0.01 0.0025" >> param_margpc.txt # init_variance: mean=0.01, std=0.0025 **Parameter Names (pnames.txt)** @@ -350,7 +350,7 @@ List parameter names matching your AMReX ParmParse inputs: echo "diffusion_coeff" > pnames.txt echo "init_amplitude" >> pnames.txt - echo "init_width" >> pnames.txt + echo "init_variance" >> pnames.txt **Output Names (outnames.txt)** diff --git a/Docs/source/HeatEquation_UQ_MathematicalDetails.rst b/Docs/source/HeatEquation_UQ_MathematicalDetails.rst index 3580bccd..e4eed034 100644 --- a/Docs/source/HeatEquation_UQ_MathematicalDetails.rst +++ b/Docs/source/HeatEquation_UQ_MathematicalDetails.rst @@ -24,12 +24,12 @@ The initial temperature profile is a Gaussian centered at (0.5, 0.5, 0.5): .. math:: - T(x,y,z,t=0) = 1 + A \exp\left(-\frac{r^2}{w^2}\right) + T(x,y,z,t=0) = 1 + A \exp\left(-\frac{r^2}{2*V}\right) where: - :math:`A` is the initial amplitude (``init_amplitude``) -- :math:`w^2` is the initial width parameter (``init_width``) +- :math:`V` is the initial variance (``init_variance``) - :math:`r^2 = (x-0.5)^2 + (y-0.5)^2 + (z-0.5)^2` Uncertain Input Parameters @@ -49,7 +49,7 @@ The three uncertain parameters in this analysis are: - Standard deviation: 0.25 K - Range: [0.25, 1.75] K -3. **init_width** (:math:`w^2`): Controls spread of initial temperature profile +3. **init_variance** (:math:`V`): Controls spread of initial temperature profile - Mean: 0.01 m² - Standard deviation: 0.0025 m² @@ -81,5 +81,5 @@ PyTUQ uses polynomial chaos expansion to construct a surrogate model: The connection is: -- **Inputs**: ParmParse parameters (``diffusion_coeff``, ``init_amplitude``, ``init_width``) specified in ``inputs`` file or command line +- **Inputs**: ParmParse parameters (``diffusion_coeff``, ``init_amplitude``, ``init_variance``) specified in ``inputs`` file or command line - **Outputs**: Quantities of interest extracted from datalog files or direct Python access to MultiFabs diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/inputs b/GuidedTutorials/HeatEquation_UQ/Case-1/inputs index 2758ec96..12094cd8 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/inputs +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/inputs @@ -13,4 +13,4 @@ datalog_int = -1 # how often to write datalog (-1 = no regular outpu # Physics parameters (these are what we vary for UQ and values here will be overwritten diffusion_coeff = 100. # diffusion coefficient for heat equation init_amplitude = 1. # amplitude of initial temperature profile -init_width = 0.01 # width parameter (variance, not std dev) +init_variance = 0.01 # variance of initial temperature profile diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp index 8a79a2e4..2314b11b 100644 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp @@ -45,8 +45,8 @@ int main (int argc, char* argv[]) // amplitude of initial temperature profile Real init_amplitude; - // width parameter controlling spread of initial profile (variance, not std dev) - Real init_width; + // variance of the intitial temperature profile + Real init_variance; // ********************************** // DECLARE DATALOG PARAMETERS @@ -106,10 +106,10 @@ int main (int argc, char* argv[]) init_amplitude = 1.0; pp.query("init_amplitude", init_amplitude); - // Width parameter - this is the variance (width²), not standard deviation + // Initial temperature variance // Smaller values = more concentrated, larger values = more spread out - init_width = 0.01; // Note: 0.01 to match your original rsquared/0.01 - pp.query("init_width", init_width); + init_variance = 0.01; + pp.query("init_variance", init_variance); } // ********************************** @@ -180,14 +180,14 @@ int main (int argc, char* argv[]) // ********************************** // SET INITIAL TEMPERATURE PROFILE // ********************************** - // Formula: T = 1 + amplitude * exp(-r^2 / width^2) + // Formula: T = 1 + amplitude * exp(-r^2 / (2*variance)) // where r is distance from center (0.5, 0.5, 0.5) // // Parameters: // - amplitude: controls peak temperature above baseline (1.0) - // - width: controls spread of initial hot spot - // - smaller width = more concentrated - // - larger width = more spread out + // - variance: controls spread of initial hot spot + // - smaller variance = more concentrated + // - larger variance = more spread out ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) { @@ -198,7 +198,7 @@ int main (int argc, char* argv[]) Real x = (i+0.5) * dx[0]; Real y = (j+0.5) * dx[1]; Real z = (k+0.5) * dx[2]; - Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/init_width; + Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/(2*init_variance); phiOld(i,j,k) = 1. + init_amplitude * std::exp(-rsquared); }); } diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm index 10467d32..9614dbbc 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm @@ -26,7 +26,7 @@ echo "0.01 0.0025" >> param_margpc.txt echo "diffusion_coeff" > pnames.txt echo "init_amplitude" >> pnames.txt -echo "init_width" >> pnames.txt +echo "init_variance" >> pnames.txt echo "max_temp" > outnames.txt echo "mean_temp" >> outnames.txt @@ -41,7 +41,7 @@ ${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER ${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM srun parallel --jobs 4 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ datalog=datalog_{#}.txt \ plot_int = -1 > /dev/null 2>&1 \ && tail -1 datalog_{#}.txt' \ diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x b/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x index 2b5dcb9a..9d35874f 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x @@ -15,7 +15,7 @@ export PUQAPPS=$UQPC/.. ## Uncomment one of them. ## (a) Given mean and standard deviation of each normal random parameter -# inputs are diffusion_coeff, init_amplitude, init_width +# inputs are diffusion_coeff, init_amplitude, init_variance echo "100 25 " > param_margpc.txt echo "1 0.25" >> param_margpc.txt echo "0.01 0.0025" >> param_margpc.txt @@ -24,7 +24,7 @@ INPC_ORDER=1 # Creates input PC coefficient file pcf.txt (will have lots of zeros since we assume independent inputs) ${PUQAPPS}/pc_prep.py -f marg -i param_margpc.txt -p ${INPC_ORDER} -# ## (b) Given mean and half-width of each uniform random parameter +# ## (b) Given mean and variance of each uniform random parameter # echo "1 0.3 " > param_margpc.txt # echo "3 0.1" >> param_margpc.txt # echo "1 0.5" >> param_margpc.txt @@ -119,7 +119,7 @@ mv psam.txt ptest.txt; mv qsam.txt qtest.txt echo "diffusion_coef" > pnames.txt echo "init_amplitude" >> pnames.txt -echo "init_width" >> pnames.txt +echo "init_variance" >> pnames.txt echo "max_temp" > outnames.txt echo "mean_temp" >> outnames.txt @@ -136,16 +136,22 @@ echo "cell_temp" >> outnames.txt # ptrain.txt is N x d input matrix, each row is a parameter vector of size d # ytrain.txt is N x o output matrix, each row is a output vector of size o -parallel --jobs 1 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ +parallel --jobs 4 --keep-order --colsep ' ' \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ datalog=datalog_{#}.txt \ > /dev/null 2>&1 \ && tail -1 datalog_{#}.txt' \ :::: ptrain.txt > ytrain.txt +#while IFS=' ' read -r diffusion_coeff init_amplitude init_variance; do +# mpiexec -n 1 ./main3d.gnu.ex inputs diffusion_coeff="$diffusion_coeff" init_amplitude="$init_amplitude" init_variance="$init_variance" datalog="datalog_$(printf '%03d' $((counter++))).txt" > /dev/null 2>&1 +# tail -1 "datalog_$(printf '%03d' $((counter - 1))).txt" +#done < ptrain.txt > ytrain.txt + + # Similar for testing parallel --jobs 1 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ datalog=datalog_{#}.txt \ > /dev/null 2>&1 \ && tail -1 datalog_{#}.txt' \ diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc_js.x b/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc_js.x index ee893ba0..8dd465d3 100755 --- a/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc_js.x +++ b/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc_js.x @@ -5,7 +5,7 @@ export KLPC=$(pwd)/../../../../pytuq #First-order Polynomial Chaos (PC). ## Given mean and standard deviation of each normal random parameter -# inputs are diffusion_coeff, init_amplitude, init_width +# inputs are diffusion_coeff, init_amplitude, init_variance echo "1 0.25 " > param_margpc.txt echo "1 0.25" >> param_margpc.txt echo "0.01 0.0025" >> param_margpc.txt @@ -24,7 +24,7 @@ ${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM # run all the jobs with psam.txt as the parameters, and log the final outputs in ysam.txt # outputs to ysam.txt are max_temp, mean_temp, std_temp, total_energy parallel --jobs 4 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_width={3} \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ datalog=datalog_{#}.txt \ > /dev/null 2>&1 \ && tail -1 datalog_{#}.txt' \ From eeb911295844fbbe4220cb4e5535c23d261de2ee Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Fri, 9 Jan 2026 17:57:44 -0800 Subject: [PATCH 127/142] typo on pyTUQ location --- Docs/source/HeatEquation_UQ.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 9763ca24..8a692810 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -60,7 +60,7 @@ Install pytuq as described in `pytuq/README.md Date: Mon, 12 Jan 2026 12:24:28 -0800 Subject: [PATCH 128/142] move location of UQ example to GuidedTutorials --- .../UQ/HeatEquation}/Case-1/GNUmakefile | 0 .../UQ/HeatEquation}/Case-1/Make.package | 0 .../UQ/HeatEquation}/Case-1/inputs | 0 .../UQ/HeatEquation}/Case-1/main.cpp | 0 .../UQ/HeatEquation}/Case-1/wf_uqpc.slurm | 0 .../UQ/HeatEquation}/Case-1/workflow_uqpc.x | 0 .../UQ/HeatEquation}/Case-1/workflow_uqpc_js.x | 0 .../UQ/HeatEquation}/Case-2/AMReXBaseModel.py | 0 .../UQ/HeatEquation}/Case-2/CMakeLists.txt | 0 .../UQ/HeatEquation}/Case-2/GNUmakefile | 0 .../UQ/HeatEquation}/Case-2/Make.package | 0 .../UQ/HeatEquation}/Case-2/ModelXBaseModel.py | 0 .../UQ/HeatEquation}/Case-2/inputs | 0 .../UQ/HeatEquation}/Case-2/main.cpp | 0 .../UQ/HeatEquation}/Case-2/model.x | 0 .../UQ/HeatEquation}/Case-2/pnames.txt | 0 .../UQ/HeatEquation}/Case-2/postprocess_datalog.sh | 0 .../UQ/HeatEquation}/Case-3/AMReXBaseModel.py | 0 .../UQ/HeatEquation}/Case-3/GNUmakefile | 0 .../UQ/HeatEquation}/Case-3/HeatEquationModel.py | 0 .../UQ/HeatEquation}/Case-3/Make.package | 0 .../UQ/HeatEquation}/Case-3/inputs | 0 .../UQ/HeatEquation}/Case-3/main.py | 0 .../UQ/HeatEquation}/example_detailed_install.sh | 0 24 files changed, 0 insertions(+), 0 deletions(-) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-1/GNUmakefile (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-1/Make.package (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-1/inputs (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-1/main.cpp (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-1/wf_uqpc.slurm (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-1/workflow_uqpc.x (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-1/workflow_uqpc_js.x (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-2/AMReXBaseModel.py (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-2/CMakeLists.txt (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-2/GNUmakefile (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-2/Make.package (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-2/ModelXBaseModel.py (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-2/inputs (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-2/main.cpp (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-2/model.x (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-2/pnames.txt (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-2/postprocess_datalog.sh (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-3/AMReXBaseModel.py (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-3/GNUmakefile (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-3/HeatEquationModel.py (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-3/Make.package (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-3/inputs (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/Case-3/main.py (100%) rename {GuidedTutorials/HeatEquation_UQ => ExampleCodes/UQ/HeatEquation}/example_detailed_install.sh (100%) diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/GNUmakefile b/ExampleCodes/UQ/HeatEquation/Case-1/GNUmakefile similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/GNUmakefile rename to ExampleCodes/UQ/HeatEquation/Case-1/GNUmakefile diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/Make.package b/ExampleCodes/UQ/HeatEquation/Case-1/Make.package similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/Make.package rename to ExampleCodes/UQ/HeatEquation/Case-1/Make.package diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/inputs b/ExampleCodes/UQ/HeatEquation/Case-1/inputs similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/inputs rename to ExampleCodes/UQ/HeatEquation/Case-1/inputs diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp b/ExampleCodes/UQ/HeatEquation/Case-1/main.cpp similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/main.cpp rename to ExampleCodes/UQ/HeatEquation/Case-1/main.cpp diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm b/ExampleCodes/UQ/HeatEquation/Case-1/wf_uqpc.slurm similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/wf_uqpc.slurm rename to ExampleCodes/UQ/HeatEquation/Case-1/wf_uqpc.slurm diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc.x rename to ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x diff --git a/GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc_js.x b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc_js.x similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-1/workflow_uqpc_js.x rename to ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc_js.x diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py b/ExampleCodes/UQ/HeatEquation/Case-2/AMReXBaseModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/AMReXBaseModel.py rename to ExampleCodes/UQ/HeatEquation/Case-2/AMReXBaseModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/CMakeLists.txt b/ExampleCodes/UQ/HeatEquation/Case-2/CMakeLists.txt similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/CMakeLists.txt rename to ExampleCodes/UQ/HeatEquation/Case-2/CMakeLists.txt diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/GNUmakefile b/ExampleCodes/UQ/HeatEquation/Case-2/GNUmakefile similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/GNUmakefile rename to ExampleCodes/UQ/HeatEquation/Case-2/GNUmakefile diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/Make.package b/ExampleCodes/UQ/HeatEquation/Case-2/Make.package similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/Make.package rename to ExampleCodes/UQ/HeatEquation/Case-2/Make.package diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/ModelXBaseModel.py b/ExampleCodes/UQ/HeatEquation/Case-2/ModelXBaseModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/ModelXBaseModel.py rename to ExampleCodes/UQ/HeatEquation/Case-2/ModelXBaseModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/inputs b/ExampleCodes/UQ/HeatEquation/Case-2/inputs similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/inputs rename to ExampleCodes/UQ/HeatEquation/Case-2/inputs diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/main.cpp b/ExampleCodes/UQ/HeatEquation/Case-2/main.cpp similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/main.cpp rename to ExampleCodes/UQ/HeatEquation/Case-2/main.cpp diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/model.x b/ExampleCodes/UQ/HeatEquation/Case-2/model.x similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/model.x rename to ExampleCodes/UQ/HeatEquation/Case-2/model.x diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/pnames.txt b/ExampleCodes/UQ/HeatEquation/Case-2/pnames.txt similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/pnames.txt rename to ExampleCodes/UQ/HeatEquation/Case-2/pnames.txt diff --git a/GuidedTutorials/HeatEquation_UQ/Case-2/postprocess_datalog.sh b/ExampleCodes/UQ/HeatEquation/Case-2/postprocess_datalog.sh similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-2/postprocess_datalog.sh rename to ExampleCodes/UQ/HeatEquation/Case-2/postprocess_datalog.sh diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/AMReXBaseModel.py b/ExampleCodes/UQ/HeatEquation/Case-3/AMReXBaseModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/AMReXBaseModel.py rename to ExampleCodes/UQ/HeatEquation/Case-3/AMReXBaseModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/GNUmakefile b/ExampleCodes/UQ/HeatEquation/Case-3/GNUmakefile similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/GNUmakefile rename to ExampleCodes/UQ/HeatEquation/Case-3/GNUmakefile diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/HeatEquationModel.py b/ExampleCodes/UQ/HeatEquation/Case-3/HeatEquationModel.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/HeatEquationModel.py rename to ExampleCodes/UQ/HeatEquation/Case-3/HeatEquationModel.py diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/Make.package b/ExampleCodes/UQ/HeatEquation/Case-3/Make.package similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/Make.package rename to ExampleCodes/UQ/HeatEquation/Case-3/Make.package diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/inputs b/ExampleCodes/UQ/HeatEquation/Case-3/inputs similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/inputs rename to ExampleCodes/UQ/HeatEquation/Case-3/inputs diff --git a/GuidedTutorials/HeatEquation_UQ/Case-3/main.py b/ExampleCodes/UQ/HeatEquation/Case-3/main.py similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/Case-3/main.py rename to ExampleCodes/UQ/HeatEquation/Case-3/main.py diff --git a/GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh b/ExampleCodes/UQ/HeatEquation/example_detailed_install.sh similarity index 100% rename from GuidedTutorials/HeatEquation_UQ/example_detailed_install.sh rename to ExampleCodes/UQ/HeatEquation/example_detailed_install.sh From 1843f58462c863ddc0ba0ede14cb5fd2293e5e8f Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Mon, 12 Jan 2026 12:27:45 -0800 Subject: [PATCH 129/142] change location of tutorial code and references to location --- Docs/source/HeatEquation_UQ.rst | 16 ++++++++-------- .../UQ/HeatEquation/Case-1/workflow_uqpc.x | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 8a692810..4534978d 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -38,7 +38,7 @@ with uncertain parameters ``diffusion_coeff`` (:math:`D`), ``init_amplitude`` (: The outputs of interest are the maximum temperature, mean temperature, standard deviation of temperature, and total "energy" (i.e., summation of temperature over all grid points). -Located in ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ``, these examples illustrate the complete forward UQ workflow from parameter sampling through sensitivity analysis. After running the provided examples, the Customizing the Workflow section explains the underlying problem setup and provides step-by-step guidance for adapting this workflow to your own AMReX application. +Located in ``amrex-tutorials/ExampleCodes/UQ/HeatEquation``, these examples illustrate the complete forward UQ workflow from parameter sampling through sensitivity analysis. After running the provided examples, the Customizing the Workflow section explains the underlying problem setup and provides step-by-step guidance for adapting this workflow to your own AMReX application. Installation ------------ @@ -85,7 +85,7 @@ Install pytuq as described in `pytuq/README.md `_ + For a detailed installation script that includes AMReX, pyAMReX, and PyTUQ setup in a conda environment, see ``ExampleCodes/UQ/HeatEquation/example_detailed_install.sh`` `example_detailed_install.sh <../../../ExampleCodes/UQ/HeatEquation/example_detailed_install.sh>`_ Examples @@ -105,7 +105,7 @@ C++ AMReX + PyTUQ (BASH driven) .. code-block:: bash :caption: Build C++ example - cd amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-1 + cd amrex-tutorials/ExampleCodes/UQ/HeatEquation/Case-1 make -j4 .. code-block:: bash @@ -287,7 +287,7 @@ C++ AMReX + PyTUQ (python driven) .. code-block:: bash :caption: Build C++ example - cd GuidedTutorials/HeatEquation_UQ/Case-2 + cd ExampleCodes/UQ/HeatEquation/Case-2 make -j4 .. note:: @@ -351,7 +351,7 @@ PyAMReX + PyTUQ .. code-block:: bash :caption: Navigate to Case-3 directory - cd GuidedTutorials/HeatEquation_UQ/Case-3 + cd ExampleCodes/UQ/HeatEquation/Case-3 .. note:: @@ -484,6 +484,6 @@ Additional Resources For complete working examples of the ``outputs = model(inputs)`` pattern, see: - - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-1/`` - C++ executable and python scripts called from a bash workflow script - - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-2/`` - C++ executable driven by python wrapping bash - - ``amrex-tutorials/GuidedTutorials/HeatEquation_UQ/Case-3/`` - PyAMReX native + - ``amrex-tutorials/ExampleCodes/UQ/HeatEquation/Case-1/`` - C++ executable and python scripts called from a bash workflow script + - ``amrex-tutorials/ExampleCodes/UQ/HeatEquation/Case-2/`` - C++ executable driven by python wrapping bash + - ``amrex-tutorials/ExampleCodes/UQ/HeatEquation/Case-3/`` - PyAMReX native diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x index 9d35874f..f1223b08 100755 --- a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x +++ b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x @@ -2,7 +2,7 @@ #===================================================================================== # Script location -export UQPC=../../../../pytuq/apps/uqpc +export UQPC=../../../../../pytuq/apps/uqpc export PUQAPPS=$UQPC/.. @@ -17,7 +17,7 @@ export PUQAPPS=$UQPC/.. ## (a) Given mean and standard deviation of each normal random parameter # inputs are diffusion_coeff, init_amplitude, init_variance echo "100 25 " > param_margpc.txt -echo "1 0.25" >> param_margpc.txt +echo "10 2.5" >> param_margpc.txt echo "0.01 0.0025" >> param_margpc.txt PC_TYPE=HG # Hermite-Gaussian PC INPC_ORDER=1 From 271e7bce922f55d305f993da4ddddefd8225b337 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Mon, 12 Jan 2026 12:53:06 -0800 Subject: [PATCH 130/142] modify problem to give D more influence --- .../UQ/HeatEquation/Case-1/workflow_uqpc.x | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x index f1223b08..25f08a13 100755 --- a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x +++ b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x @@ -16,8 +16,8 @@ export PUQAPPS=$UQPC/.. ## (a) Given mean and standard deviation of each normal random parameter # inputs are diffusion_coeff, init_amplitude, init_variance -echo "100 25 " > param_margpc.txt -echo "10 2.5" >> param_margpc.txt +echo "400 100 " > param_margpc.txt +echo "1.0 0.25" >> param_margpc.txt echo "0.01 0.0025" >> param_margpc.txt PC_TYPE=HG # Hermite-Gaussian PC INPC_ORDER=1 @@ -137,9 +137,7 @@ echo "cell_temp" >> outnames.txt # ytrain.txt is N x o output matrix, each row is a output vector of size o parallel --jobs 4 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ - datalog=datalog_{#}.txt \ - > /dev/null 2>&1 \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_{#}.txt > /dev/null 2>&1 \ && tail -1 datalog_{#}.txt' \ :::: ptrain.txt > ytrain.txt @@ -150,10 +148,8 @@ parallel --jobs 4 --keep-order --colsep ' ' \ # Similar for testing -parallel --jobs 1 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ - datalog=datalog_{#}.txt \ - > /dev/null 2>&1 \ +parallel --jobs 4 --keep-order --colsep ' ' \ + './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_{#}.txt > /dev/null 2>&1 \ && tail -1 datalog_{#}.txt' \ :::: ptest.txt > ytest.txt From b80c8d02f0b62157859f53a074460179917bef18 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Mon, 12 Jan 2026 12:57:43 -0800 Subject: [PATCH 131/142] stabilize problem with smaller dt --- ExampleCodes/UQ/HeatEquation/Case-1/inputs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/inputs b/ExampleCodes/UQ/HeatEquation/Case-1/inputs index 12094cd8..2a654017 100644 --- a/ExampleCodes/UQ/HeatEquation/Case-1/inputs +++ b/ExampleCodes/UQ/HeatEquation/Case-1/inputs @@ -3,8 +3,8 @@ n_cell = 32 # number of cells on each side of domain max_grid_size = 16 # size of each box (or grid) # Time stepping parameters -nsteps = 10 # total steps in simulation -dt = 5.e-7 # time step +nsteps = 100 # total steps in simulation +dt = 5.e-8 # time step # Output control plot_int = -1 # how often to write plotfile (-1 = no plots) From 8e5dccb36fa519ca09d01c260a00835446095505 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Wed, 14 Jan 2026 15:57:16 -0800 Subject: [PATCH 132/142] mpiexec option --- .../UQ/HeatEquation/Case-1/GNUmakefile | 2 +- .../UQ/HeatEquation/Case-1/workflow_uqpc.x | 61 +++++++++++++------ 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/GNUmakefile b/ExampleCodes/UQ/HeatEquation/Case-1/GNUmakefile index f918d4f2..90bcb595 100644 --- a/ExampleCodes/UQ/HeatEquation/Case-1/GNUmakefile +++ b/ExampleCodes/UQ/HeatEquation/Case-1/GNUmakefile @@ -2,7 +2,7 @@ AMREX_HOME ?= ../../../../amrex DEBUG = FALSE -USE_MPI = FALSE +USE_MPI = TRUE USE_OMP = FALSE COMP = gnu DIM = 3 diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x index 25f08a13..70b6a3de 100755 --- a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x +++ b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x @@ -52,7 +52,7 @@ ${PUQAPPS}/pc_prep.py -f marg -i param_margpc.txt -p ${INPC_ORDER} # Number of samples requested NTRN=111 # Training -NTST=33 # Testing +NTST=33 # Testing # Extract dimensionality d (i.e. number of input parameters) DIM=`awk 'NR==1{print NF}' pcf.txt` @@ -126,8 +126,6 @@ echo "mean_temp" >> outnames.txt echo "std_temp" >> outnames.txt echo "cell_temp" >> outnames.txt - - ################################ ## 2. Run the black-box model ## ################################ @@ -135,25 +133,52 @@ echo "cell_temp" >> outnames.txt # Run the black-box model, can be any model from R^d to R^o) # ptrain.txt is N x d input matrix, each row is a parameter vector of size d # ytrain.txt is N x o output matrix, each row is a output vector of size o +# Similar for testing +parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_{#}.txt > /dev/null 2>&1 && tail -1 datalog_{#}.txt' :::: ptrain.txt > ytrain.txt +parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_{#}.txt > /dev/null 2>&1 && tail -1 datalog_{#}.txt' :::: ptest.txt > ytest.txt -parallel --jobs 4 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_{#}.txt > /dev/null 2>&1 \ - && tail -1 datalog_{#}.txt' \ - :::: ptrain.txt > ytrain.txt +######## +# alternative using mpiexec +######## +## Initialize job index +#job_index=0 +# +## Read each line from ptrain.txt #while IFS=' ' read -r diffusion_coeff init_amplitude init_variance; do -# mpiexec -n 1 ./main3d.gnu.ex inputs diffusion_coeff="$diffusion_coeff" init_amplitude="$init_amplitude" init_variance="$init_variance" datalog="datalog_$(printf '%03d' $((counter++))).txt" > /dev/null 2>&1 -# tail -1 "datalog_$(printf '%03d' $((counter - 1))).txt" -#done < ptrain.txt > ytrain.txt - - -# Similar for testing -parallel --jobs 4 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_{#}.txt > /dev/null 2>&1 \ - && tail -1 datalog_{#}.txt' \ - :::: ptest.txt > ytest.txt +# +# # Prepare the datalog filename +# datalog_filename="datalog_${job_index}.txt" +# +# # Run the job with mpiexec +# mpiexec -n 4 ./main3d.gnu.MPI.ex inputs diffusion_coeff=$diffusion_coeff init_amplitude=$init_amplitude init_variance=$init_variance datalog=$datalog_filename /dev/null +# +# # If successful, output the last line of the datalog +# if [ $? -eq 0 ]; then +# tail -1 "$datalog_filename" >> ytrain.txt +# fi +# job_index=$((job_index+1)) +#done < ptrain.txt +# +## Initialize job index +#job_index=0 +# +## Read each line from ptest.txt +#while IFS=' ' read -r diffusion_coeff init_amplitude init_variance; do +# +# # Prepare the datalog filename +# datalog_filename="datalog_${job_index}.txt" +# +# # Run the job with mpiexec +# mpiexec -n 4 ./main3d.gnu.MPI.ex inputs diffusion_coeff=$diffusion_coeff init_amplitude=$init_amplitude init_variance=$init_variance datalog=$datalog_filename /dev/null +# +# # If successful, output the last line of the datalog +# if [ $? -eq 0 ]; then +# tail -1 "$datalog_filename" >> ytest.txt +# fi +# job_index=$((job_index+1)) +#done < ptest.txt -# This creates files ytrain.txt, ytest.txt (train/test model outputs) ############################## # 3. Build PC surrogate ## From 24d91acd87518bff9a82b4dedbc92addb9aea919 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Wed, 14 Jan 2026 16:17:09 -0800 Subject: [PATCH 133/142] souped-up script --- .../UQ/HeatEquation/Case-1/workflow_uqpc.x | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x index 70b6a3de..43f80ef6 100755 --- a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x +++ b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x @@ -134,8 +134,8 @@ echo "cell_temp" >> outnames.txt # ptrain.txt is N x d input matrix, each row is a parameter vector of size d # ytrain.txt is N x o output matrix, each row is a output vector of size o # Similar for testing -parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_{#}.txt > /dev/null 2>&1 && tail -1 datalog_{#}.txt' :::: ptrain.txt > ytrain.txt -parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_{#}.txt > /dev/null 2>&1 && tail -1 datalog_{#}.txt' :::: ptest.txt > ytest.txt +parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_train{#}.txt > stdoutlog_train{#}.txt 2>&1 ; tail -1 datalog_train{#}.txt' :::: ptrain.txt > ytrain.txt +parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_test{#}.txt > stdoutlog_test{#}.txt 2>&1 ; tail -1 datalog_test{#}.txt' :::: ptest.txt > ytest.txt ######## # alternative using mpiexec @@ -147,11 +147,12 @@ parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusio ## Read each line from ptrain.txt #while IFS=' ' read -r diffusion_coeff init_amplitude init_variance; do # -# # Prepare the datalog filename -# datalog_filename="datalog_${job_index}.txt" +# # Prepare the datalog and stdout log filenames +# datalog_filename="datalog_train${job_index}.txt" +# stdoutlog_filename="stdoutlog_train${job_index}.txt" # # # Run the job with mpiexec -# mpiexec -n 4 ./main3d.gnu.MPI.ex inputs diffusion_coeff=$diffusion_coeff init_amplitude=$init_amplitude init_variance=$init_variance datalog=$datalog_filename /dev/null +# mpiexec -n 4 ./main3d.gnu.MPI.ex inputs diffusion_coeff=$diffusion_coeff init_amplitude=$init_amplitude init_variance=$init_variance datalog=$datalog_filename $stdoutlog_filename 2>&1 # # # If successful, output the last line of the datalog # if [ $? -eq 0 ]; then @@ -166,11 +167,12 @@ parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusio ## Read each line from ptest.txt #while IFS=' ' read -r diffusion_coeff init_amplitude init_variance; do # -# # Prepare the datalog filename -# datalog_filename="datalog_${job_index}.txt" +# # Prepare the datalog and stdout log filenames +# datalog_filename="datalog_test${job_index}.txt" +# stdoutlog_filename="stdoutlog_test${job_index}.txt" # # # Run the job with mpiexec -# mpiexec -n 4 ./main3d.gnu.MPI.ex inputs diffusion_coeff=$diffusion_coeff init_amplitude=$init_amplitude init_variance=$init_variance datalog=$datalog_filename /dev/null +# mpiexec -n 4 ./main3d.gnu.MPI.ex inputs diffusion_coeff=$diffusion_coeff init_amplitude=$init_amplitude init_variance=$init_variance datalog=$datalog_filename $stdoutlog_filename 2>&1 # # # If successful, output the last line of the datalog # if [ $? -eq 0 ]; then @@ -179,7 +181,6 @@ parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusio # job_index=$((job_index+1)) #done < ptest.txt - ############################## # 3. Build PC surrogate ## ############################## From 834829160f16d44fce32a5b3e8da057543b65008 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Wed, 21 Jan 2026 15:52:28 -0800 Subject: [PATCH 134/142] add timer to main --- ExampleCodes/UQ/HeatEquation/Case-1/main.cpp | 8 ++++++++ ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x | 9 +++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/main.cpp b/ExampleCodes/UQ/HeatEquation/Case-1/main.cpp index 2314b11b..937d4981 100644 --- a/ExampleCodes/UQ/HeatEquation/Case-1/main.cpp +++ b/ExampleCodes/UQ/HeatEquation/Case-1/main.cpp @@ -16,6 +16,9 @@ int main (int argc, char* argv[]) amrex::Initialize(argc,argv); { + // store the current time so we can later compute total run time. + Real strt_time = ParallelDescriptor::second(); + // ********************************** // DECLARE SIMULATION PARAMETERS // ********************************** @@ -346,6 +349,11 @@ int main (int argc, char* argv[]) } } + // Call the timer again and compute the maximum difference between the start time + // and stop time over all processors + Real stop_time = ParallelDescriptor::second() - strt_time; + ParallelDescriptor::ReduceRealMax(stop_time); + amrex::Print() << "Run time = " << stop_time << std::endl; } Finalize(); diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x index 43f80ef6..c98c87f0 100755 --- a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x +++ b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x @@ -1,16 +1,17 @@ #!/bin/bash -e #===================================================================================== -# Script location -export UQPC=../../../../../pytuq/apps/uqpc -export PUQAPPS=$UQPC/.. +# indicate location of pytuq repo here +export PYTUQ_HOME=../../../../../pytuq +# relative location of pytuq and uqpc apps +export PUQAPPS=$PYTUQ_HOME/apps +export UQPC=$PYTUQ_HOME/apps/uqpc ################################ ## 0. Setup the problem ## ################################ - ## Four simple options for uncertain input parameter setup. ## Uncomment one of them. From 62e837d54b9d4ad84aa50b62d9ae95c3c80c4978 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Wed, 21 Jan 2026 15:54:45 -0800 Subject: [PATCH 135/142] re-org --- .../UQ/HeatEquation/Case-1/wf_uqpc.slurm | 50 ------------------- .../UQ/HeatEquation/Case-1/workflow_uqpc_js.x | 33 ------------ .../UQ/HeatEquation/{Case-1 => }/GNUmakefile | 0 .../UQ/HeatEquation/{Case-1 => }/Make.package | 0 .../UQ/HeatEquation/{Case-1 => }/inputs | 0 .../UQ/HeatEquation/{Case-1 => }/main.cpp | 0 .../HeatEquation/{Case-1 => }/workflow_uqpc.x | 0 7 files changed, 83 deletions(-) delete mode 100755 ExampleCodes/UQ/HeatEquation/Case-1/wf_uqpc.slurm delete mode 100755 ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc_js.x rename ExampleCodes/UQ/HeatEquation/{Case-1 => }/GNUmakefile (100%) rename ExampleCodes/UQ/HeatEquation/{Case-1 => }/Make.package (100%) rename ExampleCodes/UQ/HeatEquation/{Case-1 => }/inputs (100%) rename ExampleCodes/UQ/HeatEquation/{Case-1 => }/main.cpp (100%) rename ExampleCodes/UQ/HeatEquation/{Case-1 => }/workflow_uqpc.x (100%) diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/wf_uqpc.slurm b/ExampleCodes/UQ/HeatEquation/Case-1/wf_uqpc.slurm deleted file mode 100755 index 9614dbbc..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-1/wf_uqpc.slurm +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -#SBATCH --account=mp111_g -#SBATCH --nodes=1 -#SBATCH --ntasks-per-node=1 -#SBATCH -c 128 -#SBATCH --gpus-per-task=4 -#SBATCH --gpu-bind=none -#SBATCH --time=00:10:00 -#SBATCH --constraint=gpu&hbm40g -#SBATCH --qos=regular - -export MPICH_GPU_SUPPORT_ENABLED=1 -export SLURM_CPU_BIND="cores" - -# NOTE sbatch choices are altered here to make gnu parallel work with the ones in https://github.com/WeiqunZhang/amrex-scaling/blob/824551bc0189f374317bf8602bb81799deb970a2/fft/perlmutter/2025-02-06/run-16.sh -# NOTE this example is only meant for one node - -#SDIR=`dirname "$0"` -export KLPC=$(pwd)/../../../../pytuq - -#First-order Polynomial Chaos (PC). -## Given mean and standard deviation of each normal random parameter -echo "1 0.25 " > param_margpc.txt -echo "1 0.25" >> param_margpc.txt -echo "0.01 0.0025" >> param_margpc.txt - -echo "diffusion_coeff" > pnames.txt -echo "init_amplitude" >> pnames.txt -echo "init_variance" >> pnames.txt - -echo "max_temp" > outnames.txt -echo "mean_temp" >> outnames.txt -echo "std_temp" >> outnames.txt -echo "total_energy" >> outnames.txt - -PCTYPE="HG" -ORDER=1 -NSAM=111 - -${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER -${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM - -srun parallel --jobs 4 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ - datalog=datalog_{#}.txt \ - plot_int = -1 > /dev/null 2>&1 \ - && tail -1 datalog_{#}.txt' \ - :::: qsam.txt > ysam.txt - -${KLPC}/apps/pc_fit.py --pctype $PCTYPE --order $ORDER --xdata "qsam.txt" --ydata "ysam.txt" diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc_js.x b/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc_js.x deleted file mode 100755 index 8dd465d3..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc_js.x +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -e - -# location of pytuq -export KLPC=$(pwd)/../../../../pytuq - -#First-order Polynomial Chaos (PC). -## Given mean and standard deviation of each normal random parameter -# inputs are diffusion_coeff, init_amplitude, init_variance -echo "1 0.25 " > param_margpc.txt -echo "1 0.25" >> param_margpc.txt -echo "0.01 0.0025" >> param_margpc.txt - -PCTYPE="HG" -ORDER=1 -NSAM=20 - -# generate pcf.txt (re-ordering of parame_margpc.txt) -${KLPC}/apps/pc_prep.py marg param_margpc.txt $ORDER - -# generate qsam.txt (random numbers drawn from polynomial chaos basis) and -# generate psam.txt (the randomly varying inputs parameters) -${KLPC}/apps/pc_sam.py pcf.txt $PCTYPE $NSAM - -# run all the jobs with psam.txt as the parameters, and log the final outputs in ysam.txt -# outputs to ysam.txt are max_temp, mean_temp, std_temp, total_energy -parallel --jobs 4 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ - datalog=datalog_{#}.txt \ - > /dev/null 2>&1 \ - && tail -1 datalog_{#}.txt' \ - :::: psam.txt > ysam.txt - -${KLPC}/apps/pc_fit.py --pctype $PCTYPE --order $ORDER --xdata "psam.txt" --ydata "ysam.txt" diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/GNUmakefile b/ExampleCodes/UQ/HeatEquation/GNUmakefile similarity index 100% rename from ExampleCodes/UQ/HeatEquation/Case-1/GNUmakefile rename to ExampleCodes/UQ/HeatEquation/GNUmakefile diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/Make.package b/ExampleCodes/UQ/HeatEquation/Make.package similarity index 100% rename from ExampleCodes/UQ/HeatEquation/Case-1/Make.package rename to ExampleCodes/UQ/HeatEquation/Make.package diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/inputs b/ExampleCodes/UQ/HeatEquation/inputs similarity index 100% rename from ExampleCodes/UQ/HeatEquation/Case-1/inputs rename to ExampleCodes/UQ/HeatEquation/inputs diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/main.cpp b/ExampleCodes/UQ/HeatEquation/main.cpp similarity index 100% rename from ExampleCodes/UQ/HeatEquation/Case-1/main.cpp rename to ExampleCodes/UQ/HeatEquation/main.cpp diff --git a/ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x b/ExampleCodes/UQ/HeatEquation/workflow_uqpc.x similarity index 100% rename from ExampleCodes/UQ/HeatEquation/Case-1/workflow_uqpc.x rename to ExampleCodes/UQ/HeatEquation/workflow_uqpc.x From d4f03448106218e1016404ca8abb6a0d670ab268 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Wed, 21 Jan 2026 16:00:27 -0800 Subject: [PATCH 136/142] cleanup for initial public release, case-1 only --- .../UQ/HeatEquation/Case-2/AMReXBaseModel.py | 368 ------------------ .../UQ/HeatEquation/Case-2/CMakeLists.txt | 72 ---- .../UQ/HeatEquation/Case-2/GNUmakefile | 16 - .../UQ/HeatEquation/Case-2/Make.package | 2 - .../UQ/HeatEquation/Case-2/ModelXBaseModel.py | 93 ----- ExampleCodes/UQ/HeatEquation/Case-2/inputs | 16 - ExampleCodes/UQ/HeatEquation/Case-2/main.cpp | 324 --------------- ExampleCodes/UQ/HeatEquation/Case-2/model.x | 127 ------ .../UQ/HeatEquation/Case-2/pnames.txt | 3 - .../Case-2/postprocess_datalog.sh | 43 -- .../UQ/HeatEquation/Case-3/AMReXBaseModel.py | 313 --------------- .../UQ/HeatEquation/Case-3/GNUmakefile | 16 - .../HeatEquation/Case-3/HeatEquationModel.py | 251 ------------ .../UQ/HeatEquation/Case-3/Make.package | 2 - ExampleCodes/UQ/HeatEquation/Case-3/inputs | 16 - ExampleCodes/UQ/HeatEquation/Case-3/main.py | 253 ------------ .../HeatEquation/example_detailed_install.sh | 60 --- ExampleCodes/UQ/HeatEquation/workflow_uqpc.x | 110 +----- 18 files changed, 14 insertions(+), 2071 deletions(-) delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-2/AMReXBaseModel.py delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-2/CMakeLists.txt delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-2/GNUmakefile delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-2/Make.package delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-2/ModelXBaseModel.py delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-2/inputs delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-2/main.cpp delete mode 100755 ExampleCodes/UQ/HeatEquation/Case-2/model.x delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-2/pnames.txt delete mode 100755 ExampleCodes/UQ/HeatEquation/Case-2/postprocess_datalog.sh delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-3/AMReXBaseModel.py delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-3/GNUmakefile delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-3/HeatEquationModel.py delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-3/Make.package delete mode 100644 ExampleCodes/UQ/HeatEquation/Case-3/inputs delete mode 100755 ExampleCodes/UQ/HeatEquation/Case-3/main.py delete mode 100644 ExampleCodes/UQ/HeatEquation/example_detailed_install.sh diff --git a/ExampleCodes/UQ/HeatEquation/Case-2/AMReXBaseModel.py b/ExampleCodes/UQ/HeatEquation/Case-2/AMReXBaseModel.py deleted file mode 100644 index a5c20bda..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-2/AMReXBaseModel.py +++ /dev/null @@ -1,368 +0,0 @@ -from pytuq.func.func import ModelWrapperFcn -import numpy as np -from abc import abstractmethod -import os -import tempfile - -from pytuq.func.func import ModelWrapperFcn -import numpy as np -#import amrex.space3d as amr - -#def load_cupy(): -# """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" -# if amr.Config.have_gpu: -# try: -# import cupy as cp -# amr.Print("Note: found and will use cupy") -# return cp -# except ImportError: -# amr.Print("Warning: GPU found but cupy not available! Using numpy...") -# import numpy as np -# return np -# if amr.Config.gpu_backend == "SYCL": -# amr.Print("Warning: SYCL GPU backend not yet implemented for Python") -# import numpy as np -# return np -# else: -# import numpy as np -# amr.Print("Note: found and will use numpy") -# return np - -class AMReXBaseModel(ModelWrapperFcn): - """Base class for AMReX models with yt-style field info""" - - # Class-level field definitions (to be overridden by subclasses) - _field_info_class = None - _param_fields = [] - _output_fields = [] - # Requested output fields (defaults to all outputs) - _request_out_fields = None - _spatial_domain_bounds = None - - # Subprocess configuration - _model_script = './model.x' # Path to model.x wrapper script - _use_subprocess = False # Enable subprocess mode - - def __init__(self, model=None, use_subprocess=False, model_script=None, **kwargs): - # Initialize AMReX if needed -# self.xp = load_cupy() -# if not amr.initialized(): -# amr.initialize([]) - - # Configure subprocess mode - self._use_subprocess = use_subprocess or self.__class__._use_subprocess - if model_script: - self._model_script = model_script - - # Create modelpar from existing parameter information - modelpar = self._create_modelpar() - - # Setup field info container - self.field_info = self._create_field_info() - - # Setup convenience lists - self.param_names = [f[1] for f in self._param_fields] - self.output_names = [f[1] for f in self._output_fields] - - # Extract parameter bounds and create domain array for Function - param_domain = self._extract_param_domain() - - # Determine dimensions - ndim = len(self.param_names) - outdim = len(self.output_names) - - # Create model function wrapper - if model is None: - model_func = lambda params, mp=None: self._run_simulation(params) - else: - model_func = model - - # Initialize Function with model - super().__init__( - model_func, - ndim, - modelpar=modelpar, - name=kwargs.get('name', 'AMReXModel') - ) - - # Set output dimension (ModelWrapperFcn defaults to 1) - self.outdim = outdim - - # Set the parameter domain using Function's method - if param_domain is not None and len(param_domain) > 0: - self.setDimDom(domain=param_domain) - - # Setup spatial domain bounds (yt-style) - separate from parameter bounds - if self._spatial_domain_bounds: - self.domain_left_edge = self._spatial_domain_bounds[0] - self.domain_right_edge = self._spatial_domain_bounds[1] - self.domain_dimensions = (self._spatial_domain_bounds[2] - if len(self._spatial_domain_bounds) > 2 - else None) - - def _create_field_info(self): - """Create yt-style field info container""" - field_info = {} - - for field_tuple in self._param_fields: - field_info[field_tuple] = self._get_field_info(field_tuple) - - for field_tuple in self._output_fields: - field_info[field_tuple] = self._get_field_info(field_tuple) - - return field_info - - def _extract_param_domain(self): - """Extract parameter bounds into Function-compatible domain array""" - domain_list = [] - - for field_tuple in self._param_fields: - info = self._get_field_info(field_tuple) - if 'bounds' in info: - domain_list.append(info['bounds']) - else: - # Use Function's default domain size - dmax = getattr(self, 'dmax', 10.0) - domain_list.append([-dmax, dmax]) - - if domain_list: - return np.array(domain_list) - return None - - def forward(self, x): - """ - PyTorch-compatible forward method for inference. - - Args: - x: torch.Tensor or np.ndarray - Returns: - torch.Tensor if input is tensor, else np.ndarray - """ - # Check if input is PyTorch tensor - is_torch = False - if hasattr(x, 'detach'): # Duck typing for torch.Tensor - is_torch = True - import torch - x_np = x.detach().cpu().numpy() - else: - x_np = x - - # Run simulation using existing logic - outputs = self._run_simulation(x_np) - - # Convert back to torch if needed - if is_torch: - return torch.from_numpy(outputs).to(x.device) - return outputs - - def __call__(self, x): - - if x.ndim == 1: - x = x.reshape(1, -1) - self.checkDim(x) - if hasattr(self, 'domain') and self.domain is not None: - self.checkDomain(x) - - if self.modelpar is None: - outputs = self.model(x) - else: - outputs = self.model(x, self.modelpar) - - if outputs.ndim == 1: - outputs = outputs.reshape(-1, self.outdim) # ← Allows MULTIPLE outputs - - return outputs - - def _run_simulation(self, params): - """ - Core simulation logic - uses subprocess if enabled. - - Args: - params: numpy array of parameters (1D or 2D) - - Returns: - numpy array of outputs - """ - # Ensure params is 2D (n_samples x n_params) - if params.ndim == 1: - params = params.reshape(1, -1) - - n_samples = params.shape[0] - outdim = getattr(self, 'outdim', len(getattr(self, 'output_names', [])) or 1) - - if self._use_subprocess: - # Use model.x subprocess approach - outputs = self._run_subprocess(params) - else: - # Original in-process method - outputs = np.zeros((n_samples, outdim)) - - if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): - for i in range(n_samples): - sim_state = self.evolve(params[i, :]) - outputs[i, :] = self.postprocess(sim_state) - else: - raise NotImplementedError( - "Must implement _run_simulation or evolve/postprocess methods" - ) - # Return only requested outputs if specified - requested = self._request_out_fields or self._output_fields - if requested != self._output_fields: - all_names = [field[1] for field in self._output_fields] - requested_names = [field[1] for field in requested] - indices = [all_names.index(name) for name in requested_names] - return outputs[:, indices] - - return outputs - - def _run_subprocess(self, params): - """ - Run simulation using model.x subprocess (like online_bb example). - - Args: - params: 2D numpy array (n_samples x n_params) - - Returns: - 2D numpy array (n_samples x n_outputs) - """ - # Check if model.x exists - if not os.path.exists(self._model_script): - raise FileNotFoundError(f"Model script not found: {self._model_script}") - - # Create temporary input/output files - with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: - input_file = f.name - np.savetxt(f, params) - - with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: - output_file = f.name - - try: - # Run model.x - cmd = f'{self._model_script} {input_file} {output_file}' - print(f"Running: {cmd}") - exit_code = os.system(cmd) - - if exit_code != 0: - raise RuntimeError(f"Command failed with exit code {exit_code}: {cmd}") - - # Load outputs - outputs = np.loadtxt(output_file).reshape(params.shape[0], -1) - - return outputs - - finally: - # Clean up temporary files - for f in [input_file, output_file]: - if os.path.exists(f): - os.unlink(f) - - @property - def field_list(self): - """All available fields""" - return list(self.field_info.keys()) - - def _get_field_info(self, field_tuple): - """ - Override in subclass to provide field metadata. - - Args: - field_tuple: (field_type, field_name) tuple - - Returns: - dict with 'bounds', 'units', 'mean', 'std', etc. - """ - # Default empty implementation - return {} - - def get_param_info(self, param_name): - """Get info for a specific parameter""" - for field_tuple in self._param_fields: - if field_tuple[1] == param_name: - return self._get_field_info(field_tuple) - return {} - - def get_output_info(self, output_name): - """Get info for a specific output""" - for field_tuple in self._output_fields: - if field_tuple[1] == output_name: - return self._get_field_info(field_tuple) - return {} - - def _create_modelpar(self): - """Create modelpar dictionary with statistical properties""" - modelpar = { - 'param_info': {}, - 'output_info': {}, - 'param_names': [], - 'output_names': [], - 'defaults': {}, - 'bounds': {}, - 'units': {}, - 'mean': [], - 'std': [], - 'distribution': [], # 'normal', 'uniform', 'lognormal', etc. - 'pc_type': 'HG', # Hermite-Gaussian by default - } - - # Extract parameter information including statistical properties - for field_tuple in self._param_fields: - field_type, field_name = field_tuple - info = self._get_field_info(field_tuple) - - modelpar['param_info'][field_name] = info - modelpar['param_names'].append(field_name) - - # Extract statistical properties - if 'mean' in info: - modelpar['mean'].append(info['mean']) - elif 'default' in info: - modelpar['mean'].append(info['default']) - else: - # Use center of bounds if available - if 'bounds' in info: - modelpar['mean'].append(np.mean(info['bounds'])) - else: - modelpar['mean'].append(0.0) - - if 'std' in info: - modelpar['std'].append(info['std']) - else: - # Default to 10% of mean or range - if 'bounds' in info: - # Use range/6 as rough std (99.7% within bounds) - modelpar['std'].append((info['bounds'][1] - info['bounds'][0])/6.0) - else: - modelpar['std'].append(abs(modelpar['mean'][-1]) * 0.1) - - # Store other properties - if 'bounds' in info: - modelpar['bounds'][field_name] = info['bounds'] - if 'units' in info: - modelpar['units'][field_name] = info['units'] - if 'distribution' in info: - modelpar['distribution'].append(info['distribution']) - else: - modelpar['distribution'].append('normal') # default - - # Convert to numpy arrays for easier manipulation - modelpar['mean'] = np.array(modelpar['mean']) - modelpar['std'] = np.array(modelpar['std']) - - # Add output information - for field_tuple in self._output_fields: - field_type, field_name = field_tuple - info = self._get_field_info(field_tuple) - modelpar['output_info'][field_name] = info - modelpar['output_names'].append(field_name) - - return modelpar - - def write_param_marginals(self, filename='param_margpc.txt'): - """Write parameter marginals file for PC analysis""" - with open(filename, 'w') as f: - for i, name in enumerate(self.modelpar['param_names']): - mean = self.modelpar['mean'][i] - std = self.modelpar['std'][i] - f.write(f"{mean} {std}\n") - print(f"Wrote parameter marginals to {filename}") diff --git a/ExampleCodes/UQ/HeatEquation/Case-2/CMakeLists.txt b/ExampleCodes/UQ/HeatEquation/Case-2/CMakeLists.txt deleted file mode 100644 index 6fb5fbbf..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-2/CMakeLists.txt +++ /dev/null @@ -1,72 +0,0 @@ -# CMake file for build HeatEqaution_Simple tutorial. -# -# Step 1: Create and enter a build directory: -# -# mkdir build -# cd build -# -# Step 2: Configure with CMake from inside the build directory: -# -# cmake .. #if AMReX not installed -# -# or: -# -# cmake -DAMReX_ROOT=/path/to/installdir .. #if AMReX already installed -# -# Step 3: Build the configuration: -# -# cmake --build . -j4 -# -# Done! You should now see an executable called "HelloWorld". To run: -# -# mpiexec -n 4 ./HeatEqaution_Simple -# -# For additional CMake compile options see -# https://amrex-codes.github.io/amrex/docs_html/BuildingAMReX.html#building-with-cmake - -cmake_minimum_required(VERSION 3.16) - -# Project name and source file languages -project(HeatEquation_Simple - LANGUAGES C CXX) - -add_executable(HeatEquation_Simple main.cpp) - -# To use a pre-installed AMReX build, run: -# cmake -DAMReX_ROOT=/path/to/installdir -# Otherwise cmake will download AMReX from GitHub - -if(NOT DEFINED AMReX_ROOT) - message("-- Download and configure AMReX from GitHub") - - #Download AMReX from GitHub - include(FetchContent) - set(FETCHCONTENT_QUIET OFF) - - FetchContent_Declare( - amrex_code - GIT_REPOSITORY https://github.com/AMReX-Codes/amrex.git/ - GIT_TAG origin/development - ) - - FetchContent_Populate(amrex_code) - - # CMake will read the files in these directories to configure, build - # and install AMReX. - add_subdirectory(${amrex_code_SOURCE_DIR} ${amrex_code_BINARY_DIR}) - -else() - - # Add AMReX install - message("-- Searching for AMReX install directory at ${AMReX_ROOT}") - find_package(AMReX PATHS ${AMReX_ROOT}/lib/cmake/AMReX/AMReXConfig.cmake) - -endif() - -target_link_libraries(HeatEquation_Simple PRIVATE AMReX::amrex) - -## Copy input files to build directory -file( COPY ${CMAKE_SOURCE_DIR}/inputs DESTINATION . ) - -## Copy jupyter notebook for visulation to build directory -file( COPY ${CMAKE_SOURCE_DIR}/Visualization.ipynb DESTINATION . ) diff --git a/ExampleCodes/UQ/HeatEquation/Case-2/GNUmakefile b/ExampleCodes/UQ/HeatEquation/Case-2/GNUmakefile deleted file mode 100644 index f918d4f2..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-2/GNUmakefile +++ /dev/null @@ -1,16 +0,0 @@ -# AMREX_HOME defines the directory in which we will find all the AMReX code. -AMREX_HOME ?= ../../../../amrex - -DEBUG = FALSE -USE_MPI = FALSE -USE_OMP = FALSE -COMP = gnu -DIM = 3 - -include $(AMREX_HOME)/Tools/GNUMake/Make.defs - -include ./Make.package - -include $(AMREX_HOME)/Src/Base/Make.package - -include $(AMREX_HOME)/Tools/GNUMake/Make.rules diff --git a/ExampleCodes/UQ/HeatEquation/Case-2/Make.package b/ExampleCodes/UQ/HeatEquation/Case-2/Make.package deleted file mode 100644 index 7f43e5e8..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-2/Make.package +++ /dev/null @@ -1,2 +0,0 @@ -CEXE_sources += main.cpp - diff --git a/ExampleCodes/UQ/HeatEquation/Case-2/ModelXBaseModel.py b/ExampleCodes/UQ/HeatEquation/Case-2/ModelXBaseModel.py deleted file mode 100644 index 171fc6c6..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-2/ModelXBaseModel.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -import numpy as np -from typing import Tuple, List, Optional -import amrex.space3d as amr -from AMReXBaseModel import AMReXBaseModel, load_cupy - - -class HeatEquationModel(AMReXBaseModel): - """ - Heat equation simulation model with full statistical properties - and AMReX integration. - """ - - # Define parameter fields with proper naming convention - _param_fields = [ - ('param', 'diffusion_coeff'), - ('param', 'init_amplitude'), - ('param', 'init_width'), - ] - - # Define output fields - _output_fields = [ - ('output', 'max_temp'), - ('output', 'mean_temp'), - ('output', 'std_temp'), - ('output', 'total_energy'), - ] - - # Spatial domain bounds (3D heat equation domain) - _spatial_domain_bounds = [ - np.array([0.0, 0.0, 0.0]), # left edge - np.array([1.0, 1.0, 1.0]), # right edge - np.array([32, 32, 32]) # default grid dimensions - ] - - # Enable subprocess mode - _use_subprocess = True - _model_script = './model.x' - - def _get_field_info(self, field_tuple): - """ - Provide field metadata with statistical properties. - Only includes fields that are actually used by the framework. - """ - field_type, field_name = field_tuple - - field_info_dict = { - # Parameters - match param_margpc.txt values - ('param', 'diffusion_coeff'): { - 'mean': 1.0, - 'std': 0.25, - 'bounds': [0.25, 1.75], # mean ± 3*std - 'distribution': 'normal', - 'units': 'm**2/s', - 'display_name': 'Thermal Diffusivity', - }, - ('param', 'init_amplitude'): { - 'mean': 1.0, - 'std': 0.25, - 'bounds': [0.25, 1.75], # mean ± 3*std - 'distribution': 'normal', - 'units': 'K', - 'display_name': 'Initial Temperature', - }, - ('param', 'init_width'): { - 'mean': 0.01, - 'std': 0.0025, - 'bounds': [0.0025, 0.0175], # mean ± 3*std - 'distribution': 'normal', - 'units': 'm', - 'display_name': 'Initial Width', - }, - - # Outputs - just units and display names - ('output', 'max_temp'): { - 'units': 'K', - 'display_name': 'Maximum Temperature', - }, - ('output', 'mean_temp'): { - 'units': 'K', - 'display_name': 'Mean Temperature', - }, - ('output', 'std_temp'): { - 'units': 'K', - 'display_name': 'Temperature Std Dev', - }, - ('output', 'total_energy'): { - 'units': 'J', - 'display_name': 'Total Energy', - }, - } - - return field_info_dict.get(field_tuple, {}) diff --git a/ExampleCodes/UQ/HeatEquation/Case-2/inputs b/ExampleCodes/UQ/HeatEquation/Case-2/inputs deleted file mode 100644 index 56ec738e..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-2/inputs +++ /dev/null @@ -1,16 +0,0 @@ -# Grid/Domain parameters -n_cell = 32 # number of cells on each side of domain -max_grid_size = 16 # size of each box (or grid) - -# Time stepping parameters -nsteps = 100 # total steps in simulation -dt = 1.e-5 # time step - -# Output control -plot_int = 100 # how often to write plotfile (-1 = no plots) -datalog_int = -1 # how often to write datalog (-1 = no regular output) - -# Physics parameters (these are what we vary for UQ) -diffusion_coeff = 1.0 # diffusion coefficient for heat equation -init_amplitude = 1.0 # amplitude of initial temperature profile -init_width = 0.01 # width parameter (variance, not std dev) \ No newline at end of file diff --git a/ExampleCodes/UQ/HeatEquation/Case-2/main.cpp b/ExampleCodes/UQ/HeatEquation/Case-2/main.cpp deleted file mode 100644 index cf0ece87..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-2/main.cpp +++ /dev/null @@ -1,324 +0,0 @@ -/* - * A simplified single file version of the HeatEquation_EX0_C exmaple. - * This code is designed to be used with Demo_Tutorial.rst. - * - */ - - -#include -#include -#include - -int main (int argc, char* argv[]) -{ - amrex::Initialize(argc,argv); - { - - // ********************************** - // DECLARE SIMULATION PARAMETERS - // ********************************** - - // number of cells on each side of the domain - int n_cell; - - // size of each box (or grid) - int max_grid_size; - - // total steps in simulation - int nsteps; - - // how often to write a plotfile - int plot_int; - - // time step - amrex::Real dt; - - // ********************************** - // DECLARE PHYSICS PARAMETERS - // ********************************** - - // diffusion coefficient for heat equation - amrex::Real diffusion_coeff; - - // amplitude of initial temperature profile - amrex::Real init_amplitude; - - // width parameter controlling spread of initial profile (variance, not std dev) - amrex::Real init_width; - - // ********************************** - // DECLARE DATALOG PARAMETERS - // ********************************** - const int datwidth = 24; - const int datprecision = 16; - const int timeprecision = 13; - int datalog_int = -1; // Interval for regular output (<=0 means no regular output) - bool datalog_final = true; // Write datalog at final step - - // ********************************** - // READ PARAMETER VALUES FROM INPUT DATA - // ********************************** - // inputs parameters - { - // ParmParse is way of reading inputs from the inputs file - // pp.get means we require the inputs file to have it - // pp.query means we optionally need the inputs file to have it - but we must supply a default here - amrex::ParmParse pp; - - // We need to get n_cell from the inputs file - this is the number of cells on each side of - // a square (or cubic) domain. - pp.get("n_cell",n_cell); - - // The domain is broken into boxes of size max_grid_size - pp.get("max_grid_size",max_grid_size); - - // Default nsteps to 10, allow us to set it to something else in the inputs file - nsteps = 10; - pp.query("nsteps",nsteps); - - // Default plot_int to -1, allow us to set it to something else in the inputs file - // If plot_int < 0 then no plot files will be written - plot_int = -1; - pp.query("plot_int",plot_int); - - // Default datalog_int to -1, allow us to set it to something else in the inputs file - // If datalog_int < 0 then no plot files will be written - datalog_int = -1; - pp.query("datalog_int",datalog_int); - - // time step - pp.get("dt",dt); - - // ********************************** - // READ PHYSICS PARAMETERS - // ********************************** - - // Diffusion coefficient - controls how fast heat spreads - diffusion_coeff = 1.0; - pp.query("diffusion_coeff", diffusion_coeff); // Note: input name is "diffusion" but variable is "diffusion_coeff" - - // Initial temperature amplitude - init_amplitude = 1.0; - pp.query("init_amplitude", init_amplitude); - - // Width parameter - this is the variance (width²), not standard deviation - // Smaller values = more concentrated, larger values = more spread out - init_width = 0.01; // Note: 0.01 to match your original rsquared/0.01 - pp.query("init_width", init_width); - } - - // ********************************** - // DEFINE SIMULATION SETUP AND GEOMETRY - // ********************************** - - // make BoxArray and Geometry - // ba will contain a list of boxes that cover the domain - // geom contains information such as the physical domain size, - // number of points in the domain, and periodicity - amrex::BoxArray ba; - amrex::Geometry geom; - - // define lower and upper indices - amrex::IntVect dom_lo(0,0,0); - amrex::IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); - - // Make a single box that is the entire domain - amrex::Box domain(dom_lo, dom_hi); - - // Initialize the boxarray "ba" from the single box "domain" - ba.define(domain); - - // Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction - ba.maxSize(max_grid_size); - - // This defines the physical box, [0,1] in each direction. - amrex::RealBox real_box({ 0., 0., 0.}, - { 1., 1., 1.}); - - // periodic in all direction - amrex::Array is_periodic{1,1,1}; - - // This defines a Geometry object - geom.define(domain, real_box, amrex::CoordSys::cartesian, is_periodic); - - // extract dx from the geometry object - amrex::GpuArray dx = geom.CellSizeArray(); - - // Nghost = number of ghost cells for each array - int Nghost = 1; - - // Ncomp = number of components for each array - int Ncomp = 1; - - // How Boxes are distrubuted among MPI processes - amrex::DistributionMapping dm(ba); - - // we allocate two phi multifabs; one will store the old state, the other the new. - amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); - amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); - - // time = starting time in the simulation - amrex::Real time = 0.0; - - // ********************************** - // INITIALIZE DATA LOOP - // ********************************** - - // loop over boxes - for (amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi) - { - const amrex::Box& bx = mfi.validbox(); - - const amrex::Array4& phiOld = phi_old.array(mfi); - - // ********************************** - // SET INITIAL TEMPERATURE PROFILE - // ********************************** - // Formula: T = 1 + amplitude * exp(-r^2 / width^2) - // where r is distance from center (0.5, 0.5, 0.5) - // - // Parameters: - // - amplitude: controls peak temperature above baseline (1.0) - // - width: controls spread of initial hot spot - // - smaller width = more concentrated - // - larger width = more spread out - -amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) - { - // Calculate physical coordinates of cell center - amrex::Real x = (i+0.5) * dx[0]; - amrex::Real y = (j+0.5) * dx[1]; - amrex::Real z = (k+0.5) * dx[2]; - - // Calculate squared distance from domain center (0.5, 0.5, 0.5) - // Divide by init_width (which is the variance, not standard deviation) - amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / init_width; - - // Set initial temperature profile - phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); - }); - } - - // ********************************** - // WRITE DATALOG FILE - // ********************************** - if (amrex::ParallelDescriptor::IOProcessor() && (datalog_int>0 || datalog_final)) { - std::ofstream datalog("datalog.txt"); // truncate mode to start fresh - datalog << "#" << std::setw(datwidth-1) << " max_temp"; - datalog << std::setw(datwidth) << " mean_temp"; - datalog << std::setw(datwidth) << " std_temp"; - datalog << std::setw(datwidth) << " total_energy"; - datalog << std::endl; - datalog.close(); - } - - // ********************************** - // WRITE INITIAL PLOT FILE - // ********************************** - - // Write a plotfile of the initial data if plot_int > 0 - if (plot_int > 0) - { - int step = 0; - const std::string& pltfile = amrex::Concatenate("plt",step,5); - WriteSingleLevelPlotfile(pltfile, phi_old, {"phi"}, geom, time, 0); - } - - - // ********************************** - // MAIN TIME EVOLUTION LOOP - // ********************************** - - for (int step = 1; step <= nsteps; ++step) - { - // fill periodic ghost cells - phi_old.FillBoundary(geom.periodicity()); - - // new_phi = old_phi + dt * Laplacian(old_phi) - // loop over boxes - for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ) - { - const amrex::Box& bx = mfi.validbox(); - - const amrex::Array4& phiOld = phi_old.array(mfi); - const amrex::Array4& phiNew = phi_new.array(mfi); - - // advance the data by dt using heat equation with diffusion coefficient - amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) - { - // Calculate the discrete Laplacian using finite differences - amrex::Real laplacian = - (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0]) - +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1]) - +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]); - - // Apply heat equation using diffusion_coeff - matches Python version - phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; - }); - } - - // ********************************** - // INCREMENT - // ********************************** - - // update time - time = time + dt; - - // copy new solution into old solution - amrex::MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); - - // Tell the I/O Processor to write out which step we're doing - amrex::Print() << "Advanced step " << step << "\n"; - - // ********************************** - // WRITE DATALOG AT GIVEN INTERVAL - // ********************************** - - // Check if we should write datalog - bool write_datalog = false; - if (datalog_final && step == nsteps) { - write_datalog = true; // Write final step - } else if (datalog_int > 0 && step % datalog_int == 0) { - write_datalog = true; // Write every datalog_int steps - } - - if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { - std::ofstream datalog("datalog.txt", std::ios::app); - - // Calculate temperature statistics - amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); - amrex::Real max_temperature = phi_new.max(0); - amrex::Real l2_norm = phi_new.norm2(0); - amrex::Real sum_sq = l2_norm * l2_norm; - amrex::Real variance = sum_sq / phi_new.boxArray().numPts() - mean_temp * mean_temp; - amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; - amrex::Real total_energy = phi_new.sum(0); - - // Write 4 statistics - datalog << std::setw(datwidth) << std::setprecision(datprecision) << max_temperature; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << mean_temp; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << std_temperature; - datalog << std::setw(datwidth) << std::setprecision(datprecision) << total_energy; - datalog << std::endl; - - datalog.close(); - } - - // ********************************** - // WRITE PLOTFILE AT GIVEN INTERVAL - // ********************************** - - // Write a plotfile of the current data (plot_int was defined in the inputs file) - if (plot_int > 0 && step%plot_int == 0) - { - const std::string& pltfile = amrex::Concatenate("plt",step,5); - WriteSingleLevelPlotfile(pltfile, phi_new, {"phi"}, geom, time, step); - } - } - - } - amrex::Finalize(); - return 0; -} - - diff --git a/ExampleCodes/UQ/HeatEquation/Case-2/model.x b/ExampleCodes/UQ/HeatEquation/Case-2/model.x deleted file mode 100755 index ddc688f5..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-2/model.x +++ /dev/null @@ -1,127 +0,0 @@ -#!/bin/bash -# model.x - wrapper script for running simulations with datalog extraction - -# Configuration -EXE="main3d.gnu.ex" -INPUTS="inputs" -INCLUDE_HEADER=true -POSTPROCESSOR="./postprocess_datalog.sh" - -# Get arguments -INPUT_FILE="$1" -OUTPUT_FILE="$2" - -# Validate arguments -if [ -z "$INPUT_FILE" ] || [ -z "$OUTPUT_FILE" ]; then - echo "Usage: $0 " - echo "Example: $0 ptrain.txt ytrain.txt" - exit 1 -fi - -# Check required files -for file in "$INPUT_FILE" "$EXE" "$INPUTS" "$POSTPROCESSOR"; do - if [ ! -f "$file" ]; then - echo "Error: Required file $file not found!" - exit 1 - fi -done - -# Make postprocessor executable -chmod +x "$POSTPROCESSOR" - -# Read parameter names if available -if [ -f "pnames.txt" ]; then - readarray -t PARAM_NAMES < pnames.txt - echo "Parameter names: ${PARAM_NAMES[@]}" -fi - -# Function to run single simulation -run_simulation() { - local run_counter=$1 - shift - local param_values=("$@") - - # Create run directory - local run_dir="run_$(printf "%04d" $run_counter)" - mkdir -p "$run_dir" - - # Build command arguments - local cmd_args="" - if [ ${#PARAM_NAMES[@]} -gt 0 ]; then - for i in "${!PARAM_NAMES[@]}"; do - if [ $i -lt ${#param_values[@]} ]; then - cmd_args+="${PARAM_NAMES[$i]}=${param_values[$i]} " - fi - done - fi - - # Run simulation - echo "=== Running simulation $run_counter ===" >&2 - echo "Parameters: ${param_values[@]}" >&2 - - cd "$run_dir" - ../$EXE ../$INPUTS $cmd_args > simulation.log 2>&1 - local exit_code=$? - cd .. - - # Check if simulation produced output - if [ ! -f "$run_dir/datalog.txt" ]; then - echo "Error: Simulation $run_counter failed - no datalog.txt" >&2 - return 1 - fi - - # Call postprocessor - local result=$($POSTPROCESSOR "$run_dir" "$run_counter" "outnames.txt") - - if [ $? -eq 0 ]; then - echo "$result" - return 0 - else - return 1 - fi -} - -# Initialize output file -> "$OUTPUT_FILE" - -# Process all simulations -run_counter=0 -header_written=false - -while IFS= read -r line; do - # Skip empty lines and comments - [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue - - # Parse parameters - param_values=($line) - - if [ ${#param_values[@]} -eq 0 ]; then - continue - fi - - # Run simulation and get result - result=$(run_simulation $run_counter "${param_values[@]}") - - if [ $? -eq 0 ]; then - # Write header on first successful run - if [ "$header_written" = false ] && [ "$INCLUDE_HEADER" = true ] && [ -f "outnames.txt" ]; then - readarray -t output_names < outnames.txt - echo "# ${output_names[@]}" >> "$OUTPUT_FILE" - header_written=true - fi - - # Write result - echo "$result" >> "$OUTPUT_FILE" - echo "✓ Run $run_counter completed" - else - echo "✗ Run $run_counter failed" - fi - - ((run_counter++)) - -done < "$INPUT_FILE" - -echo "Completed processing $run_counter runs" -if [ -f "$OUTPUT_FILE" ]; then - echo "Output written to $OUTPUT_FILE" -fi diff --git a/ExampleCodes/UQ/HeatEquation/Case-2/pnames.txt b/ExampleCodes/UQ/HeatEquation/Case-2/pnames.txt deleted file mode 100644 index df802fcb..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-2/pnames.txt +++ /dev/null @@ -1,3 +0,0 @@ -diffusion_coeff -init_amplitude -init_width diff --git a/ExampleCodes/UQ/HeatEquation/Case-2/postprocess_datalog.sh b/ExampleCodes/UQ/HeatEquation/Case-2/postprocess_datalog.sh deleted file mode 100755 index 794807f7..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-2/postprocess_datalog.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -# postprocess_datalog.sh - Extract data from datalog.txt - -# Arguments -RUN_DIR="$1" -RUN_COUNTER="${2:-0}" -OUTNAMES_FILE="${3:-outnames.txt}" - -# Check if run directory exists -if [ ! -d "$RUN_DIR" ]; then - echo "Error: Directory $RUN_DIR not found" >&2 - exit 1 -fi - -DATALOG="$RUN_DIR/datalog.txt" - -# Check if datalog exists -if [ ! -f "$DATALOG" ]; then - echo "Error: $DATALOG not found" >&2 - exit 1 -fi - -# Generate outnames.txt from header on first run -if [ "$RUN_COUNTER" -eq 0 ] && [ ! -f "$OUTNAMES_FILE" ]; then - # Extract header line, remove #, clean up spaces - header=$(grep '^#' "$DATALOG" | head -1 | sed 's/^#//' | tr -s ' ' | sed 's/^ *//' | sed 's/ *$//') - - if [ -n "$header" ]; then - echo "$header" | tr ' ' '\n' > "$OUTNAMES_FILE" - echo "Generated $OUTNAMES_FILE from datalog header" >&2 - fi -fi - -# Extract last non-comment line -result=$(grep -v '^#' "$DATALOG" | tail -1) - -if [ -z "$result" ]; then - echo "Error: No data found in $DATALOG" >&2 - exit 1 -fi - -# Output the result -echo "$result" diff --git a/ExampleCodes/UQ/HeatEquation/Case-3/AMReXBaseModel.py b/ExampleCodes/UQ/HeatEquation/Case-3/AMReXBaseModel.py deleted file mode 100644 index 0cd3fdd9..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-3/AMReXBaseModel.py +++ /dev/null @@ -1,313 +0,0 @@ -from pytuq.func.func import ModelWrapperFcn -import numpy as np -from abc import abstractmethod - -from pytuq.func.func import ModelWrapperFcn -import numpy as np -import amrex.space3d as amr - -def load_cupy(): - """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" - if amr.Config.have_gpu: - try: - import cupy as cp - amr.Print("Note: found and will use cupy") - return cp - except ImportError: - amr.Print("Warning: GPU found but cupy not available! Using numpy...") - import numpy as np - return np - if amr.Config.gpu_backend == "SYCL": - amr.Print("Warning: SYCL GPU backend not yet implemented for Python") - import numpy as np - return np - else: - import numpy as np - amr.Print("Note: found and will use numpy") - return np - -class AMReXBaseModel(ModelWrapperFcn): - """Base class for AMReX models with yt-style field info""" - - # Class-level field definitions (to be overridden by subclasses) - _field_info_class = None - _param_fields = [] - _output_fields = [] - # Requested output fields (defaults to all outputs) - _request_out_fields = None - _spatial_domain_bounds = None - - def __init__(self, model=None, **kwargs): - # Initialize AMReX if needed - self.xp = load_cupy() - if not amr.initialized(): - amr.initialize([]) - - # Create modelpar from existing parameter information - modelpar = self._create_modelpar() - - # Setup field info container - self.field_info = self._create_field_info() - - # Setup convenience lists - self.param_names = [f[1] for f in self._param_fields] - self.output_names = [f[1] for f in self._output_fields] - - # Extract parameter bounds and create domain array for Function - param_domain = self._extract_param_domain() - - # Determine dimensions - ndim = len(self.param_names) - outdim = len(self.output_names) - - # Create model function wrapper - if model is None: - model_func = lambda params, mp=None: self._run_simulation(params) - else: - model_func = model - - # Initialize Function with model - super().__init__( - model_func, - ndim, - modelpar=modelpar, - name=kwargs.get('name', 'AMReXModel') - ) - - # Set output dimension (ModelWrapperFcn defaults to 1) - self.outdim = outdim - - # Set the parameter domain using Function's method - if param_domain is not None and len(param_domain) > 0: - self.setDimDom(domain=param_domain) - - # Setup spatial domain bounds (yt-style) - separate from parameter bounds - if self._spatial_domain_bounds: - self.domain_left_edge = self._spatial_domain_bounds[0] - self.domain_right_edge = self._spatial_domain_bounds[1] - self.domain_dimensions = (self._spatial_domain_bounds[2] - if len(self._spatial_domain_bounds) > 2 - else None) - - def _create_field_info(self): - """Create yt-style field info container""" - field_info = {} - - for field_tuple in self._param_fields: - field_info[field_tuple] = self._get_field_info(field_tuple) - - for field_tuple in self._output_fields: - field_info[field_tuple] = self._get_field_info(field_tuple) - - return field_info - - def _extract_param_domain(self): - """Extract parameter bounds into Function-compatible domain array""" - domain_list = [] - - for field_tuple in self._param_fields: - info = self._get_field_info(field_tuple) - if 'bounds' in info: - domain_list.append(info['bounds']) - else: - # Use Function's default domain size - dmax = getattr(self, 'dmax', 10.0) - domain_list.append([-dmax, dmax]) - - if domain_list: - return np.array(domain_list) - return None - - def forward(self, x): - """ - PyTorch-compatible forward method for inference. - - Args: - x: torch.Tensor or np.ndarray - Returns: - torch.Tensor if input is tensor, else np.ndarray - """ - # Check if input is PyTorch tensor - is_torch = False - if hasattr(x, 'detach'): # Duck typing for torch.Tensor - is_torch = True - import torch - x_np = x.detach().cpu().numpy() - else: - x_np = x - - # Run simulation using existing logic - outputs = self._run_simulation(x_np) - - # Convert back to torch if needed - if is_torch: - return torch.from_numpy(outputs).to(x.device) - return outputs - - def __call__(self, x): - - if x.ndim == 1: - x = x.reshape(1, -1) - self.checkDim(x) - if hasattr(self, 'domain') and self.domain is not None: - self.checkDomain(x) - - if self.modelpar is None: - outputs = self.model(x) - else: - outputs = self.model(x, self.modelpar) - - if outputs.ndim == 1: - outputs = outputs.reshape(-1, self.outdim) # ← Allows MULTIPLE outputs - - return outputs - - def _run_simulation(self, params): - """ - Core simulation logic - override in subclasses or use evolve/postprocess. - - Args: - params: numpy array of parameters (1D or 2D) - - Returns: - numpy array of outputs - """ - # Ensure params is 2D (n_samples x n_params) - if params.ndim == 1: - params = params.reshape(1, -1) - - n_samples = params.shape[0] - outdim = getattr(self, 'outdim', len(getattr(self, 'output_names', [])) or 1) - outputs = np.zeros((n_samples, outdim)) - - # Check if subclass has evolve/postprocess methods - if hasattr(self, 'evolve') and hasattr(self, 'postprocess'): - for i in range(n_samples): - # Evolve just returns simulation state - sim_state = self.evolve(params[i, :]) - # Postprocess extracts outputs from state - outputs[i, :] = self.postprocess(sim_state) - else: - raise NotImplementedError( - "Must implement _run_simulation or evolve/postprocess methods" - ) - - # Return only requested outputs if specified - requested = self._request_out_fields or self._output_fields - if requested != self._output_fields: - all_names = [field[1] for field in self._output_fields] - requested_names = [field[1] for field in requested] - indices = [all_names.index(name) for name in requested_names] - return outputs[:, indices] - - return outputs - - @property - def field_list(self): - """All available fields""" - return list(self.field_info.keys()) - - def _get_field_info(self, field_tuple): - """ - Override in subclass to provide field metadata. - - Args: - field_tuple: (field_type, field_name) tuple - - Returns: - dict with 'bounds', 'units', 'mean', 'std', etc. - """ - # Default empty implementation - return {} - - def get_param_info(self, param_name): - """Get info for a specific parameter""" - for field_tuple in self._param_fields: - if field_tuple[1] == param_name: - return self._get_field_info(field_tuple) - return {} - - def get_output_info(self, output_name): - """Get info for a specific output""" - for field_tuple in self._output_fields: - if field_tuple[1] == output_name: - return self._get_field_info(field_tuple) - return {} - - def _create_modelpar(self): - """Create modelpar dictionary with statistical properties""" - modelpar = { - 'param_info': {}, - 'output_info': {}, - 'param_names': [], - 'output_names': [], - 'defaults': {}, - 'bounds': {}, - 'units': {}, - 'mean': [], - 'std': [], - 'distribution': [], # 'normal', 'uniform', 'lognormal', etc. - 'pc_type': 'HG', # Hermite-Gaussian by default - } - - # Extract parameter information including statistical properties - for field_tuple in self._param_fields: - field_type, field_name = field_tuple - info = self._get_field_info(field_tuple) - - modelpar['param_info'][field_name] = info - modelpar['param_names'].append(field_name) - - # Extract statistical properties - if 'mean' in info: - modelpar['mean'].append(info['mean']) - elif 'default' in info: - modelpar['mean'].append(info['default']) - else: - # Use center of bounds if available - if 'bounds' in info: - modelpar['mean'].append(np.mean(info['bounds'])) - else: - modelpar['mean'].append(0.0) - - if 'std' in info: - modelpar['std'].append(info['std']) - else: - # Default to 10% of mean or range - if 'bounds' in info: - # Use range/6 as rough std (99.7% within bounds) - modelpar['std'].append((info['bounds'][1] - info['bounds'][0])/6.0) - else: - modelpar['std'].append(abs(modelpar['mean'][-1]) * 0.1) - - # Store other properties - if 'bounds' in info: - modelpar['bounds'][field_name] = info['bounds'] - if 'units' in info: - modelpar['units'][field_name] = info['units'] - if 'distribution' in info: - modelpar['distribution'].append(info['distribution']) - else: - modelpar['distribution'].append('normal') # default - - # Convert to numpy arrays for easier manipulation - modelpar['mean'] = np.array(modelpar['mean']) - modelpar['std'] = np.array(modelpar['std']) - - # Add output information - for field_tuple in self._output_fields: - field_type, field_name = field_tuple - info = self._get_field_info(field_tuple) - modelpar['output_info'][field_name] = info - modelpar['output_names'].append(field_name) - - return modelpar - - def write_param_marginals(self, filename='param_margpc.txt'): - """Write parameter marginals file for PC analysis""" - with open(filename, 'w') as f: - for i, name in enumerate(self.modelpar['param_names']): - mean = self.modelpar['mean'][i] - std = self.modelpar['std'][i] - f.write(f"{mean} {std}\n") - print(f"Wrote parameter marginals to {filename}") diff --git a/ExampleCodes/UQ/HeatEquation/Case-3/GNUmakefile b/ExampleCodes/UQ/HeatEquation/Case-3/GNUmakefile deleted file mode 100644 index f918d4f2..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-3/GNUmakefile +++ /dev/null @@ -1,16 +0,0 @@ -# AMREX_HOME defines the directory in which we will find all the AMReX code. -AMREX_HOME ?= ../../../../amrex - -DEBUG = FALSE -USE_MPI = FALSE -USE_OMP = FALSE -COMP = gnu -DIM = 3 - -include $(AMREX_HOME)/Tools/GNUMake/Make.defs - -include ./Make.package - -include $(AMREX_HOME)/Src/Base/Make.package - -include $(AMREX_HOME)/Tools/GNUMake/Make.rules diff --git a/ExampleCodes/UQ/HeatEquation/Case-3/HeatEquationModel.py b/ExampleCodes/UQ/HeatEquation/Case-3/HeatEquationModel.py deleted file mode 100644 index 1ada03f5..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-3/HeatEquationModel.py +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env python3 -import numpy as np -from typing import Tuple, List, Optional -import amrex.space3d as amr -from AMReXBaseModel import AMReXBaseModel, load_cupy - - -class HeatEquationModel(AMReXBaseModel): - """ - Heat equation simulation model with full statistical properties - and AMReX integration. - """ - - # Define parameter fields with proper naming convention - _param_fields = [ - ('param', 'diffusion_coeff'), - ('param', 'init_amplitude'), - ('param', 'init_width'), - ] - - # Define output fields - _output_fields = [ - ('output', 'max_temp'), - ('output', 'mean_temp'), - ('output', 'std_temp'), - ('output', 'total_energy'), - ] - - # Spatial domain bounds (3D heat equation domain) - _spatial_domain_bounds = [ - np.array([0.0, 0.0, 0.0]), # left edge - np.array([1.0, 1.0, 1.0]), # right edge - np.array([32, 32, 32]) # default grid dimensions - ] - - def __init__(self, n_cell: int = 32, max_grid_size: int = 16, - nsteps: int = 1000, plot_int: int = 100, - dt: float = 1e-5, use_parmparse: bool = True, **kwargs): - """ - Initialize heat equation model. - - Parameters: - ----------- - n_cell : int - Number of grid cells in each dimension - max_grid_size : int - Maximum grid size for AMR - nsteps : int - Number of time steps - plot_int : int - Plot interval (for output control) - dt : float - Time step size - use_parmparse : bool - Whether to use AMReX ParmParse for input - """ - # Store simulation parameters - if use_parmparse: - # Conditionally initialize AMReX first if using ParmParse - if not amr.initialized(): - amr.initialize([]) - - # Parse inputs from file - from main import parse_inputs - params = parse_inputs() - self.n_cell = params['n_cell'] - self.max_grid_size = params['max_grid_size'] - self.nsteps = params['nsteps'] - self.plot_int = params['plot_int'] - self.dt = params['dt'] - else: - self.n_cell = n_cell - self.max_grid_size = max_grid_size - self.nsteps = nsteps - self.plot_int = plot_int - self.dt = dt - - # Conditionally initialize AMReX - if not amr.initialized(): - amr.initialize([]) - - # Update spatial domain dimensions based on n_cell - if self.n_cell != 32: - self._spatial_domain_bounds[2] = np.array([self.n_cell, self.n_cell, self.n_cell]) - - # Initialize base class - super().__init__(**kwargs) - - def _get_field_info(self, field_tuple): - """ - Provide field metadata with statistical properties. - Only includes fields that are actually used by the framework. - """ - field_type, field_name = field_tuple - - field_info_dict = { - # Parameters - match param_margpc.txt values - ('param', 'diffusion_coeff'): { - 'mean': 1.0, - 'std': 0.25, - 'bounds': [0.1, 5.0], - 'distribution': 'normal', - 'units': 'm**2/s', - 'display_name': 'Thermal Diffusivity', - }, - ('param', 'init_amplitude'): { - 'mean': 1.0, - 'std': 0.25, - 'bounds': [0.1, 3.0], - 'distribution': 'normal', - 'units': 'K', - 'display_name': 'Initial Temperature', - }, - ('param', 'init_width'): { - 'mean': 0.01, - 'std': 0.0025, - 'bounds': [0.001, 0.1], - 'distribution': 'normal', - 'units': 'm', - 'display_name': 'Initial Width', - }, - - # Outputs - just units and display names - ('output', 'max_temp'): { - 'units': 'K', - 'display_name': 'Maximum Temperature', - }, - ('output', 'mean_temp'): { - 'units': 'K', - 'display_name': 'Mean Temperature', - }, - ('output', 'std_temp'): { - 'units': 'K', - 'display_name': 'Temperature Std Dev', - }, - ('output', 'total_energy'): { - 'units': 'J', - 'display_name': 'Total Energy', - }, - } - - return field_info_dict.get(field_tuple, {}) - - def evolve(self, param_set: np.ndarray): - """ - Run heat equation simulation. - - Parameters: - ----------- - param_set : np.ndarray - [diffusion_coeff, init_amplitude, init_width] - - Returns: - -------- - tuple : (phi_new, varnames, geom) - Ready to pass to write_single_level_plotfile - """ - from main import main # Import here to avoid circular imports - - if len(param_set) != 3: - raise ValueError(f"Expected 3 parameters, got {len(param_set)}") - - phi_new, geom = main( - diffusion_coeff=float(param_set[0]), - init_amplitude=float(param_set[1]), - init_width=float(param_set[2]), - n_cell=self.n_cell, - max_grid_size=self.max_grid_size, - nsteps=self.nsteps, - plot_int=self.plot_int, - dt=self.dt, - plot_files_output=False, - verbose=0 - ) - - varnames = amr.Vector_string(['phi']) - return phi_new, varnames, geom - - def postprocess(self, sim_state) -> np.ndarray: - """ - Heat equation specific postprocessing with geometry awareness. - - Parameters: - ----------- - multifab : amr.MultiFab - Simulation data - varnames : amr.Vector_string or None - Variable names - geom : amr.Geometry or None - Domain geometry - - Returns: - -------- - np.ndarray - Processed outputs [max, mean, std, integral/sum] - """ - xp = load_cupy() - [multifab, varnames, geom] = sim_state - - # Get basic statistics - max_val = multifab.max(comp=0, local=False) - sum_val = multifab.sum(comp=0, local=False) - total_cells = multifab.box_array().numPts - mean_val = sum_val / total_cells - - # Calculate standard deviation - l2_norm = multifab.norm2(0) - sum_sq = l2_norm**2 - variance = (sum_sq / total_cells) - mean_val**2 - std_val = np.sqrt(max(0, variance)) - - return np.array([ - float(max_val), - float(mean_val), - float(std_val), - float(sum_val) - ]) - -if __name__ == "__main__": - - # Initialize AMReX - amr.initialize([]) - - # Create model using ParmParse to read from inputs file - model = HeatEquationModel(use_parmparse=True) - - print(f"Heat equation model initialized with:") - print(f" n_cell = {model.n_cell}") - print(f" max_grid_size = {model.max_grid_size}") - print(f" nsteps = {model.nsteps}") - print(f" plot_int = {model.plot_int}") - print(f" dt = {model.dt}") - - # Test with random parameters - test_params = np.array([ - [1.0, 1.0, 0.01], # baseline - [2.0, 1.5, 0.02], # higher diffusion, higher amplitude - [0.5, 2.0, 0.005] # lower diffusion, higher amplitude, narrower - ]) - - print("\nRunning heat equation with parameters:") - print(" [diffusion, amplitude, width]") - print(test_params) - - outputs = model(test_params) - - print("\nResults [max, mean, std, integral, center]:") - print(outputs) - - # Finalize AMReX - amr.finalize() diff --git a/ExampleCodes/UQ/HeatEquation/Case-3/Make.package b/ExampleCodes/UQ/HeatEquation/Case-3/Make.package deleted file mode 100644 index 7f43e5e8..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-3/Make.package +++ /dev/null @@ -1,2 +0,0 @@ -CEXE_sources += main.cpp - diff --git a/ExampleCodes/UQ/HeatEquation/Case-3/inputs b/ExampleCodes/UQ/HeatEquation/Case-3/inputs deleted file mode 100644 index 56ec738e..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-3/inputs +++ /dev/null @@ -1,16 +0,0 @@ -# Grid/Domain parameters -n_cell = 32 # number of cells on each side of domain -max_grid_size = 16 # size of each box (or grid) - -# Time stepping parameters -nsteps = 100 # total steps in simulation -dt = 1.e-5 # time step - -# Output control -plot_int = 100 # how often to write plotfile (-1 = no plots) -datalog_int = -1 # how often to write datalog (-1 = no regular output) - -# Physics parameters (these are what we vary for UQ) -diffusion_coeff = 1.0 # diffusion coefficient for heat equation -init_amplitude = 1.0 # amplitude of initial temperature profile -init_width = 0.01 # width parameter (variance, not std dev) \ No newline at end of file diff --git a/ExampleCodes/UQ/HeatEquation/Case-3/main.py b/ExampleCodes/UQ/HeatEquation/Case-3/main.py deleted file mode 100755 index 03533917..00000000 --- a/ExampleCodes/UQ/HeatEquation/Case-3/main.py +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# Copyright 2023 The AMReX Community -# -# This file is part of AMReX. -# -# License: BSD-3-Clause-LBNL -# Authors: Revathi Jambunathan, Edoardo Zoni, Olga Shapoval, David Grote, Axel Huebl - -import amrex.space3d as amr -import numpy as np -from typing import Tuple, Dict, Union - - -def load_cupy(): - """Load appropriate array library (CuPy for GPU, NumPy for CPU).""" - if amr.Config.have_gpu: - try: - import cupy as cp - amr.Print("Note: found and will use cupy") - return cp - except ImportError: - amr.Print("Warning: GPU found but cupy not available! Trying managed memory in numpy...") - import numpy as np - return np - if amr.Config.gpu_backend == "SYCL": - amr.Print("Warning: SYCL GPU backend not yet implemented for Python") - import numpy as np - return np - else: - import numpy as np - amr.Print("Note: found and will use numpy") - return np - - -def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, - plot_int: int = 100, dt: float = 1e-5, plot_files_output: bool = False, - verbose: int = 1, diffusion_coeff: float = 1.0, init_amplitude: float = 1.0, - init_width: float = 0.01) -> Tuple[amr.MultiFab, amr.Geometry]: - """ - Run the heat equation simulation. - The main function, automatically called below if called as a script. - - Returns: - -------- - tuple : (phi_new, geom) - Final state and geometry - """ - # CPU/GPU logic - xp = load_cupy() - - # AMREX_D_DECL means "do the first X of these, where X is the dimensionality of the simulation" - dom_lo = amr.IntVect(*amr.d_decl( 0, 0, 0)) - dom_hi = amr.IntVect(*amr.d_decl(n_cell-1, n_cell-1, n_cell-1)) - - # Make a single box that is the entire domain - domain = amr.Box(dom_lo, dom_hi) - - # Make BoxArray and Geometry: - # ba contains a list of boxes that cover the domain, - # geom contains information such as the physical domain size, - # number of points in the domain, and periodicity - - # Initialize the boxarray "ba" from the single box "domain" - ba = amr.BoxArray(domain) - # Break up boxarray "ba" into chunks no larger than "max_grid_size" along a direction - ba.max_size(max_grid_size) - - # This defines the physical box, [0,1] in each direction. - real_box = amr.RealBox([*amr.d_decl( 0., 0., 0.)], [*amr.d_decl( 1., 1., 1.)]) - - # This defines a Geometry object - # periodic in all direction - coord = 0 # Cartesian - is_per = [*amr.d_decl(1,1,1)] # periodicity - geom = amr.Geometry(domain, real_box, coord, is_per); - - # Extract dx from the geometry object - dx = geom.data().CellSize() - - # Nghost = number of ghost cells for each array - Nghost = 1 - - # Ncomp = number of components for each array - Ncomp = 1 - - # How Boxes are distrubuted among MPI processes - dm = amr.DistributionMapping(ba) - - # Allocate two phi multifabs: one will store the old state, the other the new. - phi_old = amr.MultiFab(ba, dm, Ncomp, Nghost) - phi_new = amr.MultiFab(ba, dm, Ncomp, Nghost) - phi_old.set_val(0.) - phi_new.set_val(0.) - - # time = starting time in the simulation - time = 0. - - # Ghost cells - ng = phi_old.n_grow_vect - ngx = ng[0] - ngy = ng[1] - ngz = ng[2] - - # Loop over boxes - for mfi in phi_old: - bx = mfi.validbox() - # phiOld is indexed in reversed order (z,y,x) and indices are local - phiOld = xp.array(phi_old.array(mfi), copy=False) - # set phi = 1 + amplitude * e^(-(r-0.5)^2 / width) - x = (xp.arange(bx.small_end[0],bx.big_end[0]+1,1) + 0.5) * dx[0] - y = (xp.arange(bx.small_end[1],bx.big_end[1]+1,1) + 0.5) * dx[1] - z = (xp.arange(bx.small_end[2],bx.big_end[2]+1,1) + 0.5) * dx[2] - rsquared = ((z[: , xp.newaxis, xp.newaxis] - 0.5)**2 - + (y[xp.newaxis, : , xp.newaxis] - 0.5)**2 - + (x[xp.newaxis, xp.newaxis, : ] - 0.5)**2) / init_width - phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + init_amplitude * xp.exp(-rsquared) - - # Write a plotfile of the initial data if plot_int > 0 and plot_files_output - if plot_int > 0 and plot_files_output: - step = 0 - pltfile = amr.concatenate("plt", step, 5) - varnames = amr.Vector_string(['phi']) - amr.write_single_level_plotfile(pltfile, phi_old, varnames, geom, time, 0) - - for step in range(1, nsteps+1): - # Fill periodic ghost cells - phi_old.fill_boundary(geom.periodicity()) - - # new_phi = old_phi + dt * Laplacian(old_phi) - # Loop over boxes - for mfi in phi_old: - phiOld = xp.array(phi_old.array(mfi), copy=False) - phiNew = xp.array(phi_new.array(mfi), copy=False) - hix = phiOld.shape[3] - hiy = phiOld.shape[2] - hiz = phiOld.shape[1] - - # Heat equation with parameterized diffusion - # Advance the data by dt - phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( - phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] + dt * diffusion_coeff * - (( phiOld[:, ngz :-ngz , ngy :-ngy , ngx+1:hix-ngx+1] - -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] - +phiOld[:, ngz :-ngz , ngy :-ngy , ngx-1:hix-ngx-1]) / dx[0]**2 - +( phiOld[:, ngz :-ngz , ngy+1:hiy-ngy+1, ngx :-ngx ] - -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] - +phiOld[:, ngz :-ngz , ngy-1:hiy-ngy-1, ngx :-ngx ]) / dx[1]**2 - +( phiOld[:, ngz+1:hiz-ngz+1, ngy :-ngy , ngx :-ngx ] - -2*phiOld[:, ngz :-ngz , ngy :-ngy , ngx :-ngx ] - +phiOld[:, ngz-1:hiz-ngz-1, ngy :-ngy , ngx :-ngx ]) / dx[2]**2)) - - # Update time - time = time + dt - - # Copy new solution into old solution - amr.copy_mfab(dst=phi_old, src=phi_new, srccomp=0, dstcomp=0, numcomp=1, nghost=0) - - # Tell the I/O Processor to write out which step we're doing - if(verbose > 0): - amr.Print(f'Advanced step {step}\n') - - # Write a plotfile of the current data (plot_int was defined in the inputs file) - if plot_int > 0 and step%plot_int == 0 and plot_files_output: - pltfile = amr.concatenate("plt", step, 5) - varnames = amr.Vector_string(['phi']) - amr.write_single_level_plotfile(pltfile, phi_new, varnames, geom, time, step) - - return phi_new, geom - - -def parse_inputs() -> Dict[str, Union[int, float, bool]]: - """Parse inputs using AMReX ParmParse to_dict method.""" - pp = amr.ParmParse("") - - # Add inputs file if it exists - import os - inputs_file = "inputs" - if os.path.exists(inputs_file): - pp.addfile(inputs_file) - - # Default values with their types - defaults = { - 'n_cell': 32, - 'max_grid_size': 16, - 'nsteps': 1000, - 'plot_int': 100, - 'dt': 1.0e-5, - 'plot_files_output': False, - 'diffusion_coeff': 1.0, - 'init_amplitude': 1.0, - 'init_width': 0.01 - } - - try: - # Convert entire ParmParse table to Python dictionary - all_params = pp.to_dict() - - # Extract our specific parameters with proper type conversion - params = {} - for key, default_value in defaults.items(): - if key in all_params: - try: - # Convert string to appropriate type based on default - if isinstance(default_value, int): - params[key] = int(all_params[key]) - elif isinstance(default_value, float): - params[key] = float(all_params[key]) - elif isinstance(default_value, bool): - # Handle boolean conversion from string - val_str = str(all_params[key]).lower() - if val_str in ('true', '1', 'yes', 'on'): - params[key] = True - elif val_str in ('false', '0', 'no', 'off'): - params[key] = False - else: - # If unrecognized, use default - params[key] = default_value - else: - params[key] = all_params[key] - except (ValueError, TypeError) as e: - amr.Print(f"Warning: Could not convert parameter {key}='{all_params[key]}' to {type(default_value).__name__}, using default: {default_value}") - params[key] = default_value - else: - params[key] = default_value - - # Optional: print the parameters we're actually using - amr.Print("Using parameters:") - for key, value in params.items(): - amr.Print(f" {key}: {value}") - - return params - - except Exception as e: - amr.Print(f"Warning: Could not parse parameters with to_dict(): {e}") - amr.Print("Using default values") - return defaults - -if __name__ == '__main__': - # Initialize AMReX - amr.initialize([]) - - try: - # Parse inputs - params = parse_inputs() - - # Run simulation - main(**params) - - finally: - # Finalize AMReX - amr.finalize() diff --git a/ExampleCodes/UQ/HeatEquation/example_detailed_install.sh b/ExampleCodes/UQ/HeatEquation/example_detailed_install.sh deleted file mode 100644 index d57aac3c..00000000 --- a/ExampleCodes/UQ/HeatEquation/example_detailed_install.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# For NERSC: module load conda - -# 1. Clone repositories -git clone --recursive --branch v1.0.0z https://github.com/sandialabs/pytuq -git clone --branch 25.10 https://github.com/amrex-codes/pyamrex -git clone --branch 25.10 https://github.com/amrex-codes/amrex -git clone --branch development https://github.com/amrex-codes/amrex-tutorials -# Alternative: git clone --branch add_pybind_interface_test https://github.com/jmsexton03/amrex-tutorials - -# 2. Setup conda environment -# Create conda environment (use -y for non-interactive) -conda create -y --name pyamrex_pytuq python=3.11 --no-default-packages - -# For NERSC (see https://docs.nersc.gov/development/languages/python/nersc-python/#moving-your-conda-setup-to-globalcommonsoftware): -# conda create -y --prefix /global/common/software/myproject/$USER/pyamrex_pytuq python=3.11 - -conda activate pyamrex_pytuq -# For NERSC: conda activate /global/common/software/myproject/$USER/pyamrex_pytuq - -# 3. Build and install pyAMReX (developer install) -cd pyamrex - -# Set environment variable for AMReX source -export AMREX_SRC=$PWD/../amrex - -# Optional: Set compilers explicitly -# export CC=$(which clang) -# export CXX=$(which clang++) -# For GPU support: -# export CUDACXX=$(which nvcc) -# export CUDAHOSTCXX=$(which clang++) - -# Install Python requirements -python3 -m pip install -U -r requirements.txt -python3 -m pip install -v --force-reinstall --no-deps . - -# Build with cmake (includes all dimensions) -cmake -S . -B build -DAMReX_SPACEDIM="1;2;3" -DpyAMReX_amrex_src=$(pwd)/../amrex -cmake --build build --target pip_install -j 8 - -cd ../ - -# 4. Install PyTUQ -cd pytuq -python -m pip install -r requirements.txt -python -m pip install . -conda install -y dill -cd ../ - -# 5. Setup workflow files (optional - for Case 1 examples) -# mkdir rundir -# cd rundir -# tar -xf ~/workflow_uqpc.tar # Obtain from PyTUQ examples -# cd ../ - -# 6. Verify installation -conda list | grep pyamrex # Should show pyamrex 25.10 -conda list | grep pytuq # Should show pytuq 1.0.0z diff --git a/ExampleCodes/UQ/HeatEquation/workflow_uqpc.x b/ExampleCodes/UQ/HeatEquation/workflow_uqpc.x index c98c87f0..d70661fb 100755 --- a/ExampleCodes/UQ/HeatEquation/workflow_uqpc.x +++ b/ExampleCodes/UQ/HeatEquation/workflow_uqpc.x @@ -2,7 +2,7 @@ #===================================================================================== # indicate location of pytuq repo here -export PYTUQ_HOME=../../../../../pytuq +export PYTUQ_HOME=../../../../pytuq # relative location of pytuq and uqpc apps export PUQAPPS=$PYTUQ_HOME/apps @@ -12,48 +12,20 @@ export UQPC=$PYTUQ_HOME/apps/uqpc ## 0. Setup the problem ## ################################ -## Four simple options for uncertain input parameter setup. -## Uncomment one of them. - -## (a) Given mean and standard deviation of each normal random parameter +## Given mean and standard deviation of each normal random parameter # inputs are diffusion_coeff, init_amplitude, init_variance echo "400 100 " > param_margpc.txt echo "1.0 0.25" >> param_margpc.txt echo "0.01 0.0025" >> param_margpc.txt -PC_TYPE=HG # Hermite-Gaussian PC +# Hermite-Gaussian PC +PC_TYPE=HG INPC_ORDER=1 # Creates input PC coefficient file pcf.txt (will have lots of zeros since we assume independent inputs) ${PUQAPPS}/pc_prep.py -f marg -i param_margpc.txt -p ${INPC_ORDER} -# ## (b) Given mean and variance of each uniform random parameter -# echo "1 0.3 " > param_margpc.txt -# echo "3 0.1" >> param_margpc.txt -# echo "1 0.5" >> param_margpc.txt -# PC_TYPE=LU # Legendre-Uniform PC -# INPC_ORDER=1 -# # Creates input PC coefficient file pcf.txt (will have lots of zeros since we assume independent inputs) -# ${PUQAPPS}/pc_prep.py -f marg -i param_margpc.txt -p ${INPC_ORDER} - -# ## (c) Given mean and covariance of multivariate normal random parameter -# echo "3.0" > mean.txt -# echo "-2.1" >> mean.txt -# echo "1.5 0.4" > cov.txt -# echo "0.4 2.0" >> cov.txt -# PC_TYPE=HG # Hermite-Gaussian PC -# INPC_ORDER=1 -# # Creates input PC coefficient file pcf.txt -# ${PUQAPPS}/pc_prep.py -f mvn -i mean.txt -c cov.txt -# # Visualize covariance -# ${PUQAPPS}/plot_cov.py -m mean.txt -c cov.txt - -# ## (d) Given samples of inputs in psam.txt (e.g. from a prior calibration study) -# ${PUQAPPS}/pc_prep.py -f sam -i psam.txt -p ${INPC_ORDER} -# PC_TYPE=HG # Hermite-Gaussian PC -# INPC_ORDER=3 - # Number of samples requested -NTRN=111 # Training -NTST=33 # Testing +NTRN=99 # Training +NTST=20 # Testing # Extract dimensionality d (i.e. number of input parameters) DIM=`awk 'NR==1{print NF}' pcf.txt` @@ -61,67 +33,21 @@ DIM=`awk 'NR==1{print NF}' pcf.txt` # Output PC order OUTPC_ORDER=3 -#################################### -## 1-2. Online UQ with model.x ## -#################################### -## Run UQ with model.x in an online fashion (i.e. this is equivalent to the steps 1-2 below) -# Can uncomment this and comment out steps 1-2 below. -# ${UQPC}/uq_pc.py -r online_bb -c pcf.txt -x ${PC_TYPE} -d $DIM -o ${INPC_ORDER} -m anl -s rand -n $NTRN -v $NTST -t ${OUTPC_ORDER} - - -#################################### -## 1-2. Pre-run input/output ## -#################################### - -# # Quick and dirty equivalent if user already has the input/output pairs -# # Can uncomment this and comment out steps 1-2 below, IF input.txt and output.txt are available -# ln -sf input.txt pall.txt -# ln -sf output.txt yall.txt -# # Get ranges of inputs, with 10% 'cushion' from the dimension-wise extreme samples -# ${UQPC}/../awkies/getrange.x pall.txt 0.1 > param_range.txt -# # Scale the inputs -# ${UQPC}/../awkies/scale.x pall.txt from param_range.txt qall.txt - -# # This is not ideal if input/output have strictly fewer or more rows than NTRN+NTST -# head -n$NTRN pall.txt > ptrain.txt -# head -n$NTRN qall.txt > qtrain.txt -# head -n$NTRN yall.txt > ytrain.txt - -# tail -n$NTST pall.txt > ptest.txt -# tail -n$NTST qall.txt > qtest.txt -# tail -n$NTST yall.txt > ytest.txt - -################################### -## 1-3. Online UQ with model() ## -################################### -## Run UQ with model() function defined in uq_pc.py (i.e. this is equivalent to the steps 1-3 below) -# Can uncomment this and comment out steps 1-3 below. -# ${UQPC}/uq_pc.py -r online_example -c pcf.txt -x ${PC_TYPE} -d $DIM -o ${INPC_ORDER} -m anl -s rand -n $NTRN -v $NTST -t ${OUTPC_ORDER} - - - ############################### ## 1. Prepare the inputs ## ############################### # Prepare inputs for the black-box model (use input PC to generate input samples for the model) +# This creates files ptrain.txt, ptest.txt (train/test parameter inputs), qtrain.txt, qtest.txt (corresponding train/test stochastic PC inputs) ${PUQAPPS}/pc_sam.py -f pcf.txt -t ${PC_TYPE} -n $NTRN mv psam.txt ptrain.txt; mv qsam.txt qtrain.txt ${PUQAPPS}/pc_sam.py -f pcf.txt -t ${PC_TYPE} -n $NTST mv psam.txt ptest.txt; mv qsam.txt qtest.txt - -# This creates files ptrain.txt, ptest.txt (train/test parameter inputs), qtrain.txt, qtest.txt (corresponding train/test stochastic PC inputs) - - -# Optionally can provide pnames.txt and outnames.txt with input parameter names and output QoI names -# Or delete them to use generic names -#rm -f pnames.txt outnames.txt - +# create pnames.txt and outnames.txt with input parameter names and output QoI names echo "diffusion_coef" > pnames.txt echo "init_amplitude" >> pnames.txt echo "init_variance" >> pnames.txt - echo "max_temp" > outnames.txt echo "mean_temp" >> outnames.txt echo "std_temp" >> outnames.txt @@ -131,16 +57,17 @@ echo "cell_temp" >> outnames.txt ## 2. Run the black-box model ## ################################ -# Run the black-box model, can be any model from R^d to R^o) +# Run the black-box model, both training and testing +# use parallel to launch multiple jobs at a time, each using 1 MPI rank # ptrain.txt is N x d input matrix, each row is a parameter vector of size d # ytrain.txt is N x o output matrix, each row is a output vector of size o # Similar for testing parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_train{#}.txt > stdoutlog_train{#}.txt 2>&1 ; tail -1 datalog_train{#}.txt' :::: ptrain.txt > ytrain.txt parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} datalog=datalog_test{#}.txt > stdoutlog_test{#}.txt 2>&1 ; tail -1 datalog_test{#}.txt' :::: ptest.txt > ytest.txt -######## -# alternative using mpiexec -######## +################################ +# 2. Run the black-box model (alternative using mpiexec) +################################ ## Initialize job index #job_index=0 @@ -187,15 +114,8 @@ parallel --jobs 4 --keep-order --colsep ' ' './main3d.gnu.MPI.ex inputs diffusio ############################## # Build surrogate for each output (in other words, build output PC) -${UQPC}/uq_pc.py -r offline -c pcf.txt -x ${PC_TYPE} -d $DIM -o ${INPC_ORDER} -m anl -s rand -n $NTRN -v $NTST -t ${OUTPC_ORDER} - # This creates files results.pk (Python pickle file encapsulating the results) - - -# Quick equivalent but here results are saved differently -# ${PUQAPPS}/pc_fit.py -x qtrain.txt -y ytrain.txt -c ${PC_TYPE} -o ${OUTPC_ORDER} -# This creates files pcrv.pk and lregs.pk (Python pickle file encapsulating the results) - +${UQPC}/uq_pc.py -r offline -c pcf.txt -x ${PC_TYPE} -d $DIM -o ${INPC_ORDER} -m anl -s rand -n $NTRN -v $NTST -t ${OUTPC_ORDER} ################################### ## 4. Visualize the i/o data ## @@ -221,7 +141,6 @@ ${PUQAPPS}/plot_ens.py -y ytrain.txt ## 5. Postprocess the results ## ################################ - # Plot model vs PC for each output ${UQPC}/plot.py dm training testing # Plot model vs PC for each sample @@ -245,4 +164,3 @@ ${UQPC}/plot.py 1d training testing ${UQPC}/plot.py 2d # This creates .png files for visualizing PC surrogate results and sensitivity analysis - From ee612a5da59d879f994ec7cbd045b332f38c8f65 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Thu, 22 Jan 2026 15:37:03 -0800 Subject: [PATCH 137/142] make HeatEquation and HeatEquation_UQ codes look as similar as possible --- ExampleCodes/UQ/HeatEquation/main.cpp | 114 +++++++++---------- GuidedTutorials/HeatEquation_Simple/main.cpp | 5 +- 2 files changed, 53 insertions(+), 66 deletions(-) diff --git a/ExampleCodes/UQ/HeatEquation/main.cpp b/ExampleCodes/UQ/HeatEquation/main.cpp index 937d4981..735c18b6 100644 --- a/ExampleCodes/UQ/HeatEquation/main.cpp +++ b/ExampleCodes/UQ/HeatEquation/main.cpp @@ -9,16 +9,12 @@ #include #include -using namespace amrex; int main (int argc, char* argv[]) { amrex::Initialize(argc,argv); { - // store the current time so we can later compute total run time. - Real strt_time = ParallelDescriptor::second(); - // ********************************** // DECLARE SIMULATION PARAMETERS // ********************************** @@ -36,20 +32,20 @@ int main (int argc, char* argv[]) int plot_int; // time step - Real dt; + amrex::Real dt; // ********************************** // DECLARE PHYSICS PARAMETERS // ********************************** // diffusion coefficient for heat equation - Real diffusion_coeff; + amrex::Real diffusion_coeff; // amplitude of initial temperature profile - Real init_amplitude; + amrex::Real init_amplitude; // variance of the intitial temperature profile - Real init_variance; + amrex::Real init_variance; // ********************************** // DECLARE DATALOG PARAMETERS @@ -68,7 +64,7 @@ int main (int argc, char* argv[]) // ParmParse is way of reading inputs from the inputs file // pp.get means we require the inputs file to have it // pp.query means we optionally need the inputs file to have it - but we must supply a default here - ParmParse pp; + amrex::ParmParse pp; // We need to get n_cell from the inputs file - this is the number of cells on each side of // a square (or cubic) domain. @@ -123,15 +119,15 @@ int main (int argc, char* argv[]) // ba will contain a list of boxes that cover the domain // geom contains information such as the physical domain size, // number of points in the domain, and periodicity - BoxArray ba; - Geometry geom; + amrex::BoxArray ba; + amrex::Geometry geom; // define lower and upper indices - IntVect dom_lo(0,0,0); - IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); + amrex::IntVect dom_lo(0,0,0); + amrex::IntVect dom_hi(n_cell-1, n_cell-1, n_cell-1); // Make a single box that is the entire domain - Box domain(dom_lo, dom_hi); + amrex::Box domain(dom_lo, dom_hi); // Initialize the boxarray "ba" from the single box "domain" ba.define(domain); @@ -140,17 +136,17 @@ int main (int argc, char* argv[]) ba.maxSize(max_grid_size); // This defines the physical box, [0,1] in each direction. - RealBox real_box({ 0., 0., 0.}, - { 1., 1., 1.}); + amrex::RealBox real_box({ 0., 0., 0.}, + { 1., 1., 1.}); // periodic in all direction - Array is_periodic{1,1,1}; + amrex::Array is_periodic{1,1,1}; // This defines a Geometry object - geom.define(domain, real_box, CoordSys::cartesian, is_periodic); + geom.define(domain, real_box, amrex::CoordSys::cartesian, is_periodic); // extract dx from the geometry object - GpuArray dx = geom.CellSizeArray(); + amrex::GpuArray dx = geom.CellSizeArray(); // Nghost = number of ghost cells for each array int Nghost = 1; @@ -159,26 +155,26 @@ int main (int argc, char* argv[]) int Ncomp = 1; // How Boxes are distrubuted among MPI processes - DistributionMapping dm(ba); + amrex::DistributionMapping dm(ba); // we allocate two phi multifabs; one will store the old state, the other the new. - MultiFab phi_old(ba, dm, Ncomp, Nghost); - MultiFab phi_new(ba, dm, Ncomp, Nghost); - MultiFab phi_tmp(ba, dm, Ncomp, Nghost); + amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); + amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); + amrex::MultiFab phi_tmp(ba, dm, Ncomp, Nghost); // time = starting time in the simulation - Real time = 0.0; + amrex::Real time = 0.0; // ********************************** // INITIALIZE DATA LOOP // ********************************** // loop over boxes - for (MFIter mfi(phi_old); mfi.isValid(); ++mfi) + for (amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi) { - const Box& bx = mfi.validbox(); + const amrex::Box& bx = mfi.validbox(); - const Array4& phiOld = phi_old.array(mfi); + const amrex::Array4& phiOld = phi_old.array(mfi); // ********************************** // SET INITIAL TEMPERATURE PROFILE @@ -191,17 +187,17 @@ int main (int argc, char* argv[]) // - variance: controls spread of initial hot spot // - smaller variance = more concentrated // - larger variance = more spread out - ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) + amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k) { // ********************************** // SET VALUES FOR EACH CELL // ********************************** - Real x = (i+0.5) * dx[0]; - Real y = (j+0.5) * dx[1]; - Real z = (k+0.5) * dx[2]; - Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/(2*init_variance); + amrex::Real x = (i+0.5) * dx[0]; + amrex::Real y = (j+0.5) * dx[1]; + amrex::Real z = (k+0.5) * dx[2]; + amrex::Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/(2*init_variance); phiOld(i,j,k) = 1. + init_amplitude * std::exp(-rsquared); }); } @@ -209,7 +205,7 @@ int main (int argc, char* argv[]) // ********************************** // WRITE DATALOG FILE // ********************************** - if (ParallelDescriptor::IOProcessor()) { + if (amrex::ParallelDescriptor::IOProcessor()) { std::ofstream datalog(datalog_filename); // truncate mode to start fresh datalog << "#" << std::setw(datwidth-1) << " max_temp"; datalog << std::setw(datwidth) << " mean_temp"; @@ -227,7 +223,7 @@ int main (int argc, char* argv[]) if (plot_int > 0) { int step = 0; - const std::string& pltfile = Concatenate("plt",step,5); + const std::string& pltfile = amrex::Concatenate("plt",step,5); WriteSingleLevelPlotfile(pltfile, phi_old, {"phi"}, geom, time, 0); } @@ -243,15 +239,15 @@ int main (int argc, char* argv[]) // new_phi = old_phi + dt * Laplacian(old_phi) // loop over boxes - for ( MFIter mfi(phi_old); mfi.isValid(); ++mfi ) + for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ) { - const Box& bx = mfi.validbox(); + const amrex::Box& bx = mfi.validbox(); - const Array4& phiOld = phi_old.array(mfi); - const Array4& phiNew = phi_new.array(mfi); + const amrex::Array4& phiOld = phi_old.array(mfi); + const amrex::Array4& phiNew = phi_new.array(mfi); // advance the data by dt - ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) + amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k) { // ********************************** @@ -267,16 +263,16 @@ int main (int argc, char* argv[]) } // find the value in cell (9,9,9) - ReduceOps reduce_op; - ReduceData reduce_data(reduce_op); + amrex::ReduceOps reduce_op; + amrex::ReduceData reduce_data(reduce_op); using ReduceTuple = typename decltype(reduce_data)::Type; - for ( MFIter mfi(phi_old); mfi.isValid(); ++mfi ) + for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ) { - const Box& bx = mfi.validbox(); + const amrex::Box& bx = mfi.validbox(); - const Array4& phiOld = phi_old.array(mfi); - const Array4& phiNew = phi_new.array(mfi); + const amrex::Array4& phiOld = phi_old.array(mfi); + const amrex::Array4& phiNew = phi_new.array(mfi); // advance the data by dt reduce_op.eval(bx, reduce_data, @@ -290,8 +286,8 @@ int main (int argc, char* argv[]) }); } - Real cell_temperature = get<0>(reduce_data.value()); - ParallelDescriptor::ReduceRealSum(cell_temperature); + amrex::Real cell_temperature = amrex::get<0>(reduce_data.value()); + amrex::ParallelDescriptor::ReduceRealSum(cell_temperature); // ********************************** // INCREMENT @@ -301,10 +297,10 @@ int main (int argc, char* argv[]) time = time + dt; // copy new solution into old solution - MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); + amrex::MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0); // Tell the I/O Processor to write out which step we're doing - Print() << "Advanced step " << step << "\n"; + amrex::Print() << "Advanced step " << step << "\n"; // ********************************** // WRITE DATALOG AT GIVEN INTERVAL @@ -318,13 +314,13 @@ int main (int argc, char* argv[]) write_datalog = true; // Write every datalog_int steps } - MultiFab::Copy(phi_tmp, phi_new, 0, 0, 1, 0); - Real max_temperature = phi_new.max(0); - Real mean_temperature = phi_new.sum(0) / phi_new.boxArray().numPts(); + amrex::MultiFab::Copy(phi_tmp, phi_new, 0, 0, 1, 0); + amrex::Real max_temperature = phi_new.max(0); + amrex::Real mean_temperature = phi_new.sum(0) / phi_new.boxArray().numPts(); phi_tmp.plus(-mean_temperature,0,1,0); - Real std_temperature = phi_tmp.norm2(0); // compute sqrt( sum(phi_tmp_i^2) ); + amrex::Real std_temperature = phi_tmp.norm2(0); // compute sqrt( sum(phi_tmp_i^2) ); - if (write_datalog && ParallelDescriptor::IOProcessor()) { + if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { std::ofstream datalog(datalog_filename, std::ios::app); // Write 4 statistics @@ -344,18 +340,12 @@ int main (int argc, char* argv[]) // Write a plotfile of the current data (plot_int was defined in the inputs file) if (plot_int > 0 && step%plot_int == 0) { - const std::string& pltfile = Concatenate("plt",step,5); + const std::string& pltfile = amrex::Concatenate("plt",step,5); WriteSingleLevelPlotfile(pltfile, phi_new, {"phi"}, geom, time, step); } } - // Call the timer again and compute the maximum difference between the start time - // and stop time over all processors - Real stop_time = ParallelDescriptor::second() - strt_time; - ParallelDescriptor::ReduceRealMax(stop_time); - amrex::Print() << "Run time = " << stop_time << std::endl; - } - Finalize(); + amrex::Finalize(); return 0; } diff --git a/GuidedTutorials/HeatEquation_Simple/main.cpp b/GuidedTutorials/HeatEquation_Simple/main.cpp index b3772626..b1f91aac 100644 --- a/GuidedTutorials/HeatEquation_Simple/main.cpp +++ b/GuidedTutorials/HeatEquation_Simple/main.cpp @@ -90,7 +90,7 @@ int main (int argc, char* argv[]) // This defines the physical box, [0,1] in each direction. amrex::RealBox real_box({ 0., 0., 0.}, - { 1., 1., 1.}); + { 1., 1., 1.}); // periodic in all direction amrex::Array is_periodic{1,1,1}; @@ -217,10 +217,7 @@ int main (int argc, char* argv[]) } } - } amrex::Finalize(); return 0; } - - From 04485b2c53c15150154c3e79eac81b160f601b8e Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Thu, 22 Jan 2026 15:38:02 -0800 Subject: [PATCH 138/142] comment --- ExampleCodes/UQ/HeatEquation/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ExampleCodes/UQ/HeatEquation/main.cpp b/ExampleCodes/UQ/HeatEquation/main.cpp index 735c18b6..1f383936 100644 --- a/ExampleCodes/UQ/HeatEquation/main.cpp +++ b/ExampleCodes/UQ/HeatEquation/main.cpp @@ -160,7 +160,7 @@ int main (int argc, char* argv[]) // we allocate two phi multifabs; one will store the old state, the other the new. amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost); amrex::MultiFab phi_new(ba, dm, Ncomp, Nghost); - amrex::MultiFab phi_tmp(ba, dm, Ncomp, Nghost); + amrex::MultiFab phi_tmp(ba, dm, Ncomp, Nghost); // temporary used to calculate standard deviation // time = starting time in the simulation amrex::Real time = 0.0; From e2b10a77e0aa5198b469e76330e15da4cb52e0fb Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Thu, 22 Jan 2026 16:06:33 -0800 Subject: [PATCH 139/142] significant refactor of docs to simplify for first release --- Docs/source/HeatEquation_UQ.rst | 411 ++---------------- .../HeatEquation_UQ_ExtendingTutorial.rst | 405 ----------------- .../HeatEquation_UQ_MathematicalDetails.rst | 51 +-- Docs/source/index.rst | 2 +- 4 files changed, 37 insertions(+), 832 deletions(-) delete mode 100644 Docs/source/HeatEquation_UQ_ExtendingTutorial.rst diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 4534978d..78498e3d 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -4,27 +4,26 @@ .. _pytuq_quickstart: -AMReX-pytuq +UQ with PyTUQ =========== -.. admonition:: **Time to Complete**: 15-20 minutes +.. admonition:: **Time to Complete**: 30-60 minutes :class: note **What you will learn**: - Install PyTUQ - - Run AMReX + PyTUQ examples - - Deploy on Perlmutter + - Run an AMReX+PyTUQ to perform sensitivity analysis Overview -------- -AMReX simulations deliver high-fidelity, accurate results for complex physics problems, but parameter studies and uncertainty quantification can require hundreds or thousands of runs—making comprehensive analysis computationally prohibitive. +AMReX simulations deliver high-fidelity, accurate results for complex physics problems, but the effect on simulation results due to uncertain inputs can require hundreds or thousands of simulations, making comprehensive analysis computationally prohibitive. This tutorial demonstrates how to improve efficiency without sacrificing accuracy by using polynomial chaos expansions to build fast surrogate models that identify which input parameters truly matter. PyTUQ (Python interface to the UQ Toolkit) provides specialized tools for surrogate construction and global sensitivity analysis, enabling rapid parameter space exploration and dimensionality reduction for scientific applications. -We demonstrate how to integrate PyTUQ with your AMReX application through three practical workflows: C++ executables managed with bash run management (Case-1), Python-driven C++ executables (Case-2), and native PyAMReX applications (Case-3). +We demonstrate how to integrate PyTUQ with your AMReX application through a practical workflow; the AMReX-based heat equation tutorial is equipped to perform sensitivity analysis. In these examples we model the heat equation @@ -34,31 +33,30 @@ with initial condition .. math:: \phi_0 = 1 + A e^{-r^2 / (2V)}, -with uncertain parameters ``diffusion_coeff`` (:math:`D`), ``init_amplitude`` (:math:`A`), and ``init_variance`` (:math:`V`). -The outputs of interest are the maximum temperature, mean temperature, standard deviation of temperature, -and total "energy" (i.e., summation of temperature over all grid points). +where ``r`` is the distance from the center of the domain, and with uncertain parameters ``diffusion_coeff`` (:math:`D`), ``init_amplitude`` (:math:`A`), and ``init_variance`` (:math:`V`). +The outputs of interest are the maximum temperature, mean temperature, standard deviation of temperature, and the temperature at a specified point. -Located in ``amrex-tutorials/ExampleCodes/UQ/HeatEquation``, these examples illustrate the complete forward UQ workflow from parameter sampling through sensitivity analysis. After running the provided examples, the Customizing the Workflow section explains the underlying problem setup and provides step-by-step guidance for adapting this workflow to your own AMReX application. +.. toctree:: + :maxdepth: 1 -Installation ------------- + HeatEquation_UQ_MathematicalDetails + +Located in ``amrex-tutorials/ExampleCodes/UQ/HeatEquation``, this example illustrates a complete forward UQ workflow from parameter sampling randomized input parameters to perform sensitivity analysis. +By understanding this example, you will have a basis for understanding how to adapt this workflow to your own AMReX application. -Install pytuq as described in `pytuq/README.md `_: +More specifically, you can directly compare/diff ``amrex-tutorials/ExampleCodes/UQ/HeatEquation/main.cpp`` against the original heat equation tutorial ``amrex-tutorials/GuidedTutorials/HeatEquation_Simple/main.cpp`` to see exactly what source code changes are made to the AMReX application in this example. -.. note:: +Installation +------------ - For NERSC users, consider placing your conda environment in ``/global/common/software`` - for better performance and persistence. Also, remember to ``module load conda`` before creating environments and installing. See the `NERSC Python documentation - `_ - for details. +We now describe the installation and workflow process on a local workstation. +First, install pytuq using this script (based on information provided in `pytuq/README.md `_): .. code-block:: bash :caption: Pytuq installation script #!/bin/bash - # For NERSC: module load conda - # 1. Clone repositories git clone https://github.com/sandialabs/pytuq.git @@ -67,11 +65,6 @@ Install pytuq as described in `pytuq/README.md `_ - - Examples -------- C++ AMReX + PyTUQ (BASH driven) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. dropdown:: Build and Run - :open: - **Prerequisites**: - AMReX and pytuq cloned at the same directory level as amrex-tutorials - - GNU Parallel for task management: ``sudo apt-get install parallel`` + - pytuq installation described above + - GNU Parallel for task management: ``sudo apt-get install parallel`` (mpiexec option also exists in script) .. code-block:: bash - :caption: Build C++ example + :caption: Build AMReX appplication - cd amrex-tutorials/ExampleCodes/UQ/HeatEquation/Case-1 + cd /path/to/amrex-tutorials/ExampleCodes/UQ/HeatEquation/ make -j4 .. code-block:: bash - :caption: Run with bash script + :caption: Run sensitivity analysis with bash script ./workflow_uqpc.x -.. dropdown:: Understanding GNU Parallel Workflow Pattern +Understanding GNU Parallel Workflow Pattern +------------------------------------------- The ``workflow_uqpc.x`` bash script relies on the user augmenting their codes to write outputs of interest to ASCII text files. In this case, the ``main.cpp`` was modified from the ``amrex-tutorials/GuidedTutorials/HeatEquation_Simple/main.cpp`` in the following ways: @@ -126,341 +111,13 @@ C++ AMReX + PyTUQ (BASH driven) the outputs of interest after the final step are those that matter, and are extracted by the bash script to create a master output file containing a separate set of simulation outputs of interest in each row. - The bash script calls PyTUQ scripts that generate an input parameter file (``ptrain.txt``) and then uses GNU Parallel to run multiple simulations - efficiently and collect outputs into a results file (``ytrain.txt``) that PyTUQ can use for surrogate model fitting. - - Here's how the script works. - In the first part of the script, the user generates a ``param_margpc.txt`` file containing the mean and standard deviation - of each normal random parameter. Then pyTUQ scripts are called to generate the appropriate sampling based on polynomial chaos settings, - and ultimately creates ``ptrain.txt`` which contains the parameters (one set per line) for each simulation to be run. - - .. code-block:: bash - :caption: Example GNU Parallel command (``workflow_uqpc.x``) - - #!/bin/bash -e - #===================================================================================== - - # Script location - export UQPC=../../../../pytuq/apps/uqpc - export PUQAPPS=$UQPC/.. - - ################################ - ## 0. Setup the problem ## - ################################ - - ## Four simple options for uncertain input parameter setup. - ## Uncomment one of them. - - ## (a) Given mean and standard deviation of each normal random parameter - # inputs are diffusion_coeff, init_amplitude, init_variance - echo "100 25 " > param_margpc.txt - echo "1 0.25" >> param_margpc.txt - echo "0.01 0.0025" >> param_margpc.txt - PC_TYPE=HG # Hermite-Gaussian PC - INPC_ORDER=1 - # Creates input PC coefficient file pcf.txt (will have lots of zeros since we assume independent inputs) - ${PUQAPPS}/pc_prep.py -f marg -i param_margpc.txt -p ${INPC_ORDER} - - # Number of samples requested - NTRN=111 # Training - NTST=33 # Testing - - # Extract dimensionality d (i.e. number of input parameters) - DIM=`awk 'NR==1{print NF}' pcf.txt` - - # Output PC order - OUTPC_ORDER=3 - - # Prepare inputs for the black-box model (use input PC to generate input samples for the model) - ${PUQAPPS}/pc_sam.py -f pcf.txt -t ${PC_TYPE} -n $NTRN - mv psam.txt ptrain.txt; mv qsam.txt qtrain.txt - ${PUQAPPS}/pc_sam.py -f pcf.txt -t ${PC_TYPE} -n $NTST - mv psam.txt ptest.txt; mv qsam.txt qtest.txt - - # This creates files ptrain.txt, ptest.txt (train/test parameter inputs), qtrain.txt, qtest.txt (corresponding train/test stochastic PC inputs) - - # Optionally can provide pnames.txt and outnames.txt with input parameter names and output QoI names - # Or delete them to use generic names - rm -f pnames.txt outnames.txt - - The next part of the script runs all of the simulation and collects the results. - - .. code-block:: bash - - ################################ - ## 2. Run the black-box model ## - ################################ - - # Run the black-box model, can be any model from R^d to R^o) - # ptrain.txt is N x d input matrix, each row is a parameter vector of size d - # ytrain.txt is N x o output matrix, each row is a output vector of size o - - parallel --jobs 1 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ - datalog=datalog_{#}.txt \ - > /dev/null 2>&1 \ - && tail -1 datalog_{#}.txt' \ - :::: ptrain.txt > ytrain.txt - - # Similar for testing - parallel --jobs 1 --keep-order --colsep ' ' \ - './main3d.gnu.ex inputs diffusion_coeff={1} init_amplitude={2} init_variance={3} \ - datalog=datalog_{#}.txt \ - > /dev/null 2>&1 \ - && tail -1 datalog_{#}.txt' \ - :::: ptest.txt > ytest.txt - - # This creates files ytrain.txt, ytest.txt (train/test model outputs) - - **How it works:** - - 1. **Input file (ptrain.txt)** - Space/tab-separated parameter samples (one row per simulation): - - .. code-block:: text - - 1.252744433619e+00 1.189332992765e+00 1.107922825308e-02 - 1.153626977108e+00 8.912357108037e-01 8.456030290466e-03 - 3.265959848625e-01 7.285478940917e-01 7.687549687284e-03 - - 2. **For each row**, GNU Parallel: - - - Substitutes ``{1}`` with column 1 - - Substitutes ``{2}`` with column 2 - - Substitutes ``{3}`` with column 3 - - Substitutes ``{#}`` with the line number (e.g., ``1``) → ``datalog_1.txt`` - - 3. **After simulation**, extracts output from final line of simulation standard output with ``tail -1 datalog_{#}.txt`` - - 4. **Output file (ytrain.txt)** - Collected results (one row per simulation): - - .. code-block:: text - - 1.106358248808103 0.007723115686160915 0.0562709919848521 253.0710548041209 - 0.8113473575862051 0.003858932516788893 0.03451729840856418 126.4495007101384 - 0.6608518630828316 0.002734417468982227 0.02636512039600031 89.60139162360962 - 1.082407360836961 0.01225809407946649 0.06904148772504816 401.6732267959578 - 1.279158901768254 0.006549137494533053 0.05630882962684351 214.6021374208591 - - **Key options:** - - - ``--jobs 4`` - Run 4 simulations in parallel - - ``--colsep ' '`` - Columns separated by spaces (use ``'\t'`` for tabs) - - ``-k`` - Keep output order matching input order (important for parallel jobs) - - ``::::`` - Read input from file (``ptrain.txt``) - - **Output extraction alternatives:** - - Instead of ``tail -1 datalog_{#}.txt``, you can use other extraction methods (see :ref:`pytuq_extending_tutorial` for output examples): - - - **Plotfile tools**: ``fextrema.gnu.ex plt_{#} | tail -1 | awk '{print $3}'`` - - **Custom post-processing script**: ``./postprocess_run.sh {#}`` - - A post-processing script can take the line number ``{#}`` as an argument to locate and process the corresponding simulation outputs (datalog, plotfile, etc.), then print the space/tab-separated row for that simulation. - - This pattern makes it easy to: - - - Take a parameter file with N rows (one per simulation) - - Run simulations in parallel with unique output files - - Collect results into an output file with N rows (one per simulation result) - - Finally, the pyTUQ analyzes the results: - - .. code-block:: bash - - ############################## - # 3. Build PC surrogate ## - ############################## - - # Build surrogate for each output (in other words, build output PC) - ${UQPC}/uq_pc.py -r offline -c pcf.txt -x ${PC_TYPE} -d $DIM -o ${INPC_ORDER} -m anl -s rand -n $NTRN -v $NTST -t ${OUTPC_ORDER} - -.. dropdown:: Understanding the Output - - -C++ AMReX + PyTUQ (python driven) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. dropdown:: Build and Run - - **Prerequisites**: AMReX compiled with MPI support - - .. code-block:: bash - :caption: Build C++ example - - cd ExampleCodes/UQ/HeatEquation/Case-2 - make -j4 - - .. note:: - - **Adapting the PyTUQ example script:** - - Copy the PyTUQ example script and replace the Ishigami test function with the HeatEquationModel wrapper. This allows PyTUQ to drive the C++ executable through Python subprocess calls. - - .. code-block:: bash - :caption: Copy PyTUQ example script + The bash script calls PyTUQ scripts that generate an input parameter files for training and testing (``ptrain.txt`` and ``ptest.txt``) + based on polynomial chaos settings, and then uses GNU Parallel to run multiple simulations + efficiently and collect outputs into results files (``ytrain.txt`` and ``ytest.txt``) that PyTUQ can use for surrogate model fitting. - cp ../../../../pytuq/examples/ex_pcgsa.py . - - **Modify ex_pcgsa.py:** - - Add the import near the top of the file: - - .. code-block:: python - - from ModelXBaseModel import HeatEquationModel - - Replace the model definition: - - .. code-block:: python - :caption: Original (around line 23) - - myfunc = Ishigami() - - .. code-block:: python - :caption: Replacement - - myfunc = HeatEquationModel() - myfunc._request_out_fields = [('output', 'max_temp')] - - .. code-block:: bash - :caption: Run UQ analysis - - python ./ex_pcgsa.py - - .. note:: - - **Offline Workflow (similar to Case-1):** - - Case-2 can also be run in offline mode where you manually generate training data, then fit the surrogate model. This is useful for running simulations on HPC systems and post-processing locally: - - .. code-block:: bash - :caption: Generate training data offline - - # Call model.x on parameter samples to generate outputs - ./model.x ptrain.txt ytrain.txt - - The ``model.x`` wrapper script manages calling your C++ executable for each parameter set and collecting the outputs. After generating ``ytrain.txt``, use PyTUQ's ``pc_fit.py`` to construct the surrogate model. - -PyAMReX + PyTUQ -~~~~~~~~~~~~~~~ - -.. dropdown:: Setup and Run - - **Prerequisites**: pyAMReX installed - - .. code-block:: bash - :caption: Navigate to Case-3 directory - - cd ExampleCodes/UQ/HeatEquation/Case-3 - - .. note:: - - **Using PyAMReX with PyTUQ:** - - For native PyAMReX applications, copy the PyTUQ example script and replace the Ishigami function with the PyAMReX-based HeatEquationModel. This enables direct Python-to-Python integration without subprocess overhead. - - .. code-block:: bash - :caption: Copy PyTUQ example script - - cp ../../../../pytuq/examples/ex_pcgsa.py . - - **Modify ex_pcgsa.py:** - - Add the import near the top of the file: - - .. code-block:: python - - from HeatEquationModel import HeatEquationModel - - Replace the model definition: - - .. code-block:: python - :caption: Original (around line 23) - - myfunc = Ishigami() - - .. code-block:: python - :caption: Replacement - - myfunc = HeatEquationModel() - myfunc._request_out_fields = [('output', 'max_temp')] - - .. code-block:: bash - :caption: Run UQ analysis - - python ./ex_pcgsa.py - -Perlmutter Deployment ---------------------- - -C++ AMReX on Perlmutter -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. dropdown:: Perlmutter Setup - - .. code-block:: bash - :caption: Virtual environment setup - - module load conda - # For NERSC (see https://docs.nersc.gov/development/languages/python/nersc-python/#moving-your-conda-setup-to-globalcommonsoftware): - # conda create -y --prefix /global/common/software/myproject/$USER/pytuq_integration python=3.11 - conda create -y --name pytuq_integration python=3.11 - git clone --recursive --branch v1.0.0z https://github.com/sandialabs/pytuq - cd pytuq - echo "dill" >> requirements.txt - pip install -r requirements.txt - pip install . - - .. code-block:: bash - :caption: perlmutter_build.sh - - module load PrgEnv-gnu cudatoolkit - make USE_MPI=TRUE USE_CUDA=TRUE - - .. code-block:: bash - :caption: Submit job - - # If stored in common software: conda activate /global/common/software/myproject/$USER/pytuq_integration - conda activate pytuq_integration - sbatch wk_uqpc.slurm - - .. note:: - - For NERSC, consider placing your conda environment in ``/global/common/software`` - for better performance and persistence. See the `NERSC Python documentation - `_ - for details. - -.. note:: - - **Advanced Workflow Management:** - - For more complex UQ workflows requiring sophisticated task management, consider these strategies: - - - **SLURM Job Arrays**: For running many similar parameter sweep jobs - `NERSC Job Arrays Documentation `_ - - **Hydra Submitit Launcher**: For configuration-driven HPC job submission - `Hydra Submitit Usage `_ - - **libEnsemble**: For dynamic ensemble management and adaptive sampling - `libEnsemble Platforms Guide `_ - - See the `NERSC Workflow Documentation `_ for the latest recommendations on workflow management strategies. - -Customizing the Workflow +Understanding the Output ------------------------ -.. toctree:: - :maxdepth: 1 - - HeatEquation_UQ_MathematicalDetails - HeatEquation_UQ_ExtendingTutorial - -Summary -------- - -**Key Takeaways:** - -* PyTUQ can use models with ``outputs = model(inputs)`` interface -* C++ codes can use wrapper scripts or gnu parallel; Python codes integrate directly -* Best practices for Perlmutter requires python environment modules - Additional Resources -------------------- @@ -473,17 +130,7 @@ Additional Resources **AMReX Resources:** - `AMReX Documentation `_ -- `pyAMReX Documentation `_ -- :ref:`_guided_heat` - Base tutorial this builds upon **Uncertainty Quantification Theory:** - Ghanem, Roger, David Higdon, and Houman Owhadi, eds. *Handbook of Uncertainty Quantification*. Vol. 6. New York: Springer, 2017. (For workflow, plotting, and analysis specifics) - -.. seealso:: - - For complete working examples of the ``outputs = model(inputs)`` pattern, see: - - - ``amrex-tutorials/ExampleCodes/UQ/HeatEquation/Case-1/`` - C++ executable and python scripts called from a bash workflow script - - ``amrex-tutorials/ExampleCodes/UQ/HeatEquation/Case-2/`` - C++ executable driven by python wrapping bash - - ``amrex-tutorials/ExampleCodes/UQ/HeatEquation/Case-3/`` - PyAMReX native diff --git a/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst b/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst deleted file mode 100644 index 574b750e..00000000 --- a/Docs/source/HeatEquation_UQ_ExtendingTutorial.rst +++ /dev/null @@ -1,405 +0,0 @@ -.. _pytuq_extending_tutorial: - -Extending this approach from existing applications -================================================== - -This section describes how HeatEquation_Simple and the pyamrex HeatEquation from HeatEquation_EX0_C can be modified step by step to use with pytuq to give a framework for making a similar extension for other AMReX codes. - -Identify / Add input parameters --------------------------------- - -**Implementation Steps:** - -1. Add physics parameters to main.cpp declarations: - - .. code-block:: cpp - - amrex::Real diffusion_coeff; - amrex::Real init_amplitude; - amrex::Real init_variance; - -2. Read parameters from inputs file: - - .. code-block:: cpp - - diffusion_coeff = 1.0; - pp.query("diffusion_coeff", diffusion_coeff); - - init_amplitude = 1.0; - pp.query("init_amplitude", init_amplitude); - - init_variance = 0.01; - pp.query("init_variance", init_variance); - -3. Use parameters in initial conditions and evolution: - - .. code-block:: cpp - - // Initial conditions - amrex::Real rsquared = ((x-0.5)*(x-0.5) + (y-0.5)*(y-0.5) + (z-0.5)*(z-0.5)) / (2.*init_variance); - phiOld(i,j,k) = 1.0 + init_amplitude * std::exp(-rsquared); - - // Evolution - phiNew(i,j,k) = phiOld(i,j,k) + dt * diffusion_coeff * laplacian; - -The simplest output extraction method is described here: - -Output example: C++ Datalog ----------------------------- - -.. admonition:: When to use - :class: tip - - Choose when you have centralized output locations and want simple I/O. - -**Implementation Steps:** - -1. Add datalog configuration to C++ code: - - .. code-block:: cpp - - const int datwidth = 14; - const int datprecision = 6; - int datalog_int = -1; - bool datalog_final = true; - pp.query("datalog_int", datalog_int); - -2. Write statistics to datalog.txt: - - .. code-block:: cpp - - // Check if we should write datalog - bool write_datalog = false; - if (datalog_final && step == nsteps) { - write_datalog = true; // Write final step - } else if (datalog_int > 0 && step % datalog_int == 0) { - write_datalog = true; // Write every datalog_int steps - } - - if (write_datalog && amrex::ParallelDescriptor::IOProcessor()) { - std::ofstream datalog("datalog.txt", std::ios::app); - - amrex::Real mean_temp = phi_new.sum(0) / phi_new.boxArray().numPts(); - amrex::Real max_temperature = phi_new.max(0); - amrex::Real variance = phi_new.norm2(0) / phi_new.boxArray().numPts() - mean_temp * mean_temp; - amrex::Real std_temperature = (variance > 0.0) ? std::sqrt(variance) : 0.0; - - datalog << time << " " << max_temperature << " " << std_temperature << " " << step << std::endl; - } - - .. note:: - - Some AMReX codes already have datalog capabilities: - - - For existing datalogs, you may only need to ``tail -n 1 datalog`` for the final values - - For AMR codes, use the built-in DataLog: - - .. code-block:: cpp - - // Assuming 'amr' is your Amr object - if (amrex::ParallelDescriptor::IOProcessor()) { - amr.DataLog(0) << "# time max_temperature mean_temp" << std::endl; - amr.DataLog(0) << time << " " << max_temperature << " " << mean_temp << std::endl; - } - -Output example: Plotfiles with AMReX Tools -------------------------------------------- - -.. admonition:: When to use - :class: tip - - Choose when you already write plotfiles and want to use AMReX's built-in analysis tools. This works well for offline workflows (like Case-1) where simulations run on HPC and post-processing happens locally. - -AMReX provides several compiled tools in ``amrex/Tools/Plotfile/`` for extracting information from plotfiles without writing custom code. - -**Key Tools:** - -- ``fextrema``: Extract minimum and maximum values for each variable -- ``fextract``: Extract 1D slices through plotfiles -- ``fvarnames``: List variable names in a plotfile -- ``fcompare``: Compare two plotfiles - -**Implementation Steps:** - -1. Ensure your simulation writes plotfiles: - - .. code-block:: cpp - - // In your main time-stepping loop - if (plot_int > 0 && (step % plot_int == 0 || step == nsteps)) { - const std::string& pltfile = amrex::Concatenate("plt", step, 5); - WriteSingleLevelPlotfile(pltfile, phi_new, varnames, geom, time, step); - } - -2. Build the AMReX plotfile tools (one-time setup): - - .. code-block:: bash - - cd $AMREX_HOME/Tools/Plotfile - make -j4 - - This creates executables like ``fextrema.gnu.ex``, ``fextract.gnu.ex``, etc. - -3. Use ``fextrema`` to extract min/max values: - - .. code-block:: bash - - # Extract extrema for all variables - ./fextrema.gnu.ex plt00100/ - - # Output: - # plotfile = plt00100/ - # time = 0.001000000000000002 - # variables minimum value maximum value - # phi 1 1.5801622094 - -4. Parse the output in your UQ workflow script: - - .. code-block:: bash - :caption: In wf_uqpc.x or model.x wrapper - - #!/bin/bash - # Run simulation - ./heat_equation_exec inputs diffusion_coeff=$1 init_amplitude=$2 - - # Option 1: Analyze a specific plotfile (if you know the timestep) - FINAL_PLT="plt00100" - - # Option 2: Find the final plotfile dynamically - FINAL_PLT=$(ls -d plt[0-9]* | tail -1) - - # Option 3: Use a more sophisticated approach (adapted from AMReX-Astro/workflow) - # get_last_plotfile() { - # dir=${1:-.} # Use current directory if no argument - # num=${2:-1} # How far back to go (1 = most recent) - # ls -d $dir/plt[0-9]* 2>/dev/null | sort -t plt -k 2 -g | tail -$num | head -1 - # } - # FINAL_PLT=$(get_last_plotfile . 1) - - # Extract maximum temperature using fextrema - # Use tail -1 to get the last line, then awk to extract column 3 (ignoring variable name) - MAX_TEMP=$(./fextrema.gnu.ex $FINAL_PLT | tail -1 | awk '{print $3}') - - # Output in format expected by PyTUQ (space or tab separated) - echo "$MAX_TEMP" - - .. note:: - - **Parsing strategies:** - - - **Simple approach**: Use ``tail -1`` to get the last line of output, then extract columns 2 and 3 (min/max) with ``awk``, ignoring the variable name in column 1 - - **Robust approach**: Use ``fvarnames`` to get variable names first, then use ``fextrema -v varname`` to extract specific variables - - For multiple variables, process each separately and combine into space/tab-separated output - -5. Example: Extracting multiple quantities from plotfile: - - .. code-block:: bash - :caption: extract_from_plotfile.sh - - #!/bin/bash - PLOTFILE=$1 - FEXTREMA_PATH="$AMREX_HOME/Tools/Plotfile/fextrema.gnu.ex" - - # Extract max value (column 3 from last line) - MAX_VAL=$($FEXTREMA_PATH $PLOTFILE | tail -1 | awk '{print $3}') - - # Extract min value (column 2 from last line) - MIN_VAL=$($FEXTREMA_PATH $PLOTFILE | tail -1 | awk '{print $2}') - - # Output as space-separated values - echo "$MAX_VAL $MIN_VAL" - -.. seealso:: - - - AMReX Plotfile Tools: ``$AMREX_HOME/Tools/Plotfile/`` - - Each tool prints usage information when called with no arguments: ``./fextract.gnu.ex`` - - For robust plotfile/checkpoint selection utilities, see `AMReX-Astro workflow get_last_checkpoint `_ - -Extending to PyAMReX Applications ----------------------------------- - -For PyAMReX applications, adapt your existing ``main.py`` to enable UQ parameter sweeps: - -**1. Modify main() function signature** - -Add UQ parameters as function arguments: - -.. code-block:: python - :caption: Original - - def main(n_cell, max_grid_size, nsteps, plot_int, dt): - -.. code-block:: python - :caption: With UQ parameters - - def main(n_cell: int = 32, max_grid_size: int = 16, nsteps: int = 100, - plot_int: int = 100, dt: float = 1e-5, plot_files_output: bool = False, - verbose: int = 1, diffusion_coeff: float = 1.0, init_amplitude: float = 1.0, - init_variance: float = 0.01) -> Tuple[amr.MultiFab, amr.Geometry]: - -**2. Use parameters in physics** - -Replace hardcoded values with function parameters: - -.. code-block:: python - :caption: Original initial condition (main.py:117-118) - - rsquared = ((x[:, xp.newaxis, xp.newaxis] - 0.5)**2 - + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 - + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / 0.01 - phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + xp.exp(-rsquared) - -.. code-block:: python - :caption: Parameterized - - rsquared = ((x[:, xp.newaxis, xp.newaxis] - 0.5)**2 - + (y[xp.newaxis, :, xp.newaxis] - 0.5)**2 - + (x[xp.newaxis, xp.newaxis, :] - 0.5)**2) / init_variance - phiOld[:, ngz:-ngz, ngy:-ngy, ngx:-ngx] = 1. + init_amplitude * xp.exp(-rsquared) - -.. code-block:: python - :caption: Original evolution (main.py:143-144) - - phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( - phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] - + dt*((phiOld[:, ngz:-ngz, ngy:-ngy, ngx+1:hix-ngx+1] - ... ) / dx[0]**2)) - -.. code-block:: python - :caption: With diffusion coefficient - - phiNew[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] = ( - phiOld[:, ngz:-ngz,ngy:-ngy,ngx:-ngx] - + dt * diffusion_coeff * ((phiOld[:, ngz:-ngz, ngy:-ngy, ngx+1:hix-ngx+1] - ... ) / dx[0]**2)) - -**3. Return simulation state** - -Modify the main function to return MultiFab and Geometry: - -.. code-block:: python - - return phi_new, geom - -**4. Create PyTUQ model wrapper** - -Create a ``HeatEquationModel.py`` that inherits from ``AMReXBaseModel``: - -.. code-block:: python - - from AMReXBaseModel import AMReXBaseModel - import amrex.space3d as amr - import numpy as np - - class HeatEquationModel(AMReXBaseModel): - _param_fields = [ - ('param', 'diffusion_coeff'), - ('param', 'init_amplitude'), - ('param', 'init_variance'), - ] - - _output_fields = [ - ('output', 'max_temp'), - ('output', 'mean_temp'), - ('output', 'std_temp'), - ('output', 'total_energy'), - ] - - def evolve(self, param_set: np.ndarray): - """Run simulation with given parameters.""" - from main import main - phi_new, geom = main( - diffusion_coeff=float(param_set[0]), - init_amplitude=float(param_set[1]), - init_variance=float(param_set[2]), - plot_files_output=False, - verbose=0 - ) - varnames = amr.Vector_string(['phi']) - return phi_new, varnames, geom - - def postprocess(self, sim_state) -> np.ndarray: - """Extract quantities of interest.""" - multifab, varnames, geom = sim_state - max_val = multifab.max(comp=0, local=False) - sum_val = multifab.sum(comp=0, local=False) - mean_val = sum_val / multifab.box_array().numPts - # Calculate std dev... - return np.array([max_val, mean_val, std_val, sum_val]) - -See ``Case-3/HeatEquationModel.py`` for the complete implementation. - -Configure PyTUQ Parameters ---------------------------- - -PyTUQ requires configuration files specifying uncertain parameters and output quantities of interest. For BASH-driven workflows (Case-1), create these files directly: - -**Parameter Configuration (param_margpc.txt)** - -Specify mean and standard deviation for each uncertain parameter (one per line): - -.. code-block:: bash - - echo "1 0.25 " > param_margpc.txt # diffusion_coeff: mean=1.0, std=0.25 - echo "1 0.25" >> param_margpc.txt # init_amplitude: mean=1.0, std=0.25 - echo "0.01 0.0025" >> param_margpc.txt # init_variance: mean=0.01, std=0.0025 - -**Parameter Names (pnames.txt)** - -List parameter names matching your AMReX ParmParse inputs: - -.. code-block:: bash - - echo "diffusion_coeff" > pnames.txt - echo "init_amplitude" >> pnames.txt - echo "init_variance" >> pnames.txt - -**Output Names (outnames.txt)** - -Specify quantities of interest to extract from simulation outputs: - -.. code-block:: bash - - echo "max_temp" > outnames.txt - echo "mean_temp" >> outnames.txt - echo "std_temp" >> outnames.txt - echo "total_energy" >> outnames.txt - -**Polynomial Chaos Configuration** - -Set the polynomial chaos type and order: - -.. code-block:: bash - - PCTYPE="HG" # Hermite-Gaussian for normal distributions - ORDER=1 # First-order polynomial chaos - NSAM=111 # Number of samples (depends on dimensionality and order) - -.. note:: - - **Polynomial Chaos Types:** - - - ``HG`` (Hermite-Gaussian): For normal/Gaussian distributions - - ``LU`` (Legendre-Uniform): For uniform distributions - - ``LG`` (Laguerre-Gamma): For gamma distributions - - **Sample Count:** For ``d`` dimensions and order ``p``, you need at least ``(p+d)!/(p!*d!)`` samples. For 3D with order 1: minimum 4 samples, recommended 111+. - -**PyTUQ Workflow Steps** - -The workflow consists of three stages: - -1. **Prepare PC basis** - Use ``pc_prep.py`` to initialize polynomial chaos basis functions based on parameter distributions - -2. **Sample parameter space** - Use ``pc_sam.py`` to generate parameter samples (creates ``qsam.txt``) - -3. **Fit surrogate model** - Use ``pc_fit.py`` to construct polynomial chaos expansion from simulation outputs - -.. note:: - - **Case-2 Flexibility:** - - Case-2 (Python-wrapped C++) supports both approaches: - - - **BASH file configuration** (like Case-1): Use text files (``param_margpc.txt``, ``pnames.txt``, ``outnames.txt``) with a ``model.x`` wrapper script that specifies which C++ executable and inputs file to use - - **Python metadata**: Define parameter distributions in the model class's ``_get_field_info()`` method - - Case-3 (native PyAMReX) requires Python metadata since there's no separate executable to wrap. diff --git a/Docs/source/HeatEquation_UQ_MathematicalDetails.rst b/Docs/source/HeatEquation_UQ_MathematicalDetails.rst index e4eed034..6db430cb 100644 --- a/Docs/source/HeatEquation_UQ_MathematicalDetails.rst +++ b/Docs/source/HeatEquation_UQ_MathematicalDetails.rst @@ -10,52 +10,15 @@ The Heat Equation The governing equation for this tutorial is the heat diffusion equation: -.. math:: +.. math:: \frac{\partial\phi}{\partial t} = D\nabla^2 \phi, - \frac{\partial T}{\partial t} = D \nabla^2 T + S(x,y,z) +with initial condition -where: +.. math:: \phi_0 = 1 + A e^{-r^2 / (2V)}, -- :math:`T` is the temperature field -- :math:`D` is the diffusion coefficient (``diffusion_coeff``) -- :math:`S(x,y,z)` is an optional source term (not used in this example) +where ``r`` is the distance from the center of the domain, and with uncertain parameters ``diffusion_coeff`` (:math:`D`), ``init_amplitude`` (:math:`A`), and ``init_variance`` (:math:`V`). -The initial temperature profile is a Gaussian centered at (0.5, 0.5, 0.5): -.. math:: - - T(x,y,z,t=0) = 1 + A \exp\left(-\frac{r^2}{2*V}\right) - -where: - -- :math:`A` is the initial amplitude (``init_amplitude``) -- :math:`V` is the initial variance (``init_variance``) -- :math:`r^2 = (x-0.5)^2 + (y-0.5)^2 + (z-0.5)^2` - -Uncertain Input Parameters -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The three uncertain parameters in this analysis are: - -1. **diffusion_coeff** (:math:`D`): Controls how fast heat spreads through the domain - - - Mean: 1.0 m²/s - - Standard deviation: 0.25 m²/s - - Range: [0.25, 1.75] m²/s - -2. **init_amplitude** (:math:`A`): Peak temperature above baseline - - - Mean: 1.0 K - - Standard deviation: 0.25 K - - Range: [0.25, 1.75] K - -3. **init_variance** (:math:`V`): Controls spread of initial temperature profile - - - Mean: 0.01 m² - - Standard deviation: 0.0025 m² - - Range: [0.0025, 0.0175] m² - -These parameters are specified in the AMReX inputs file and read using ``ParmParse::query()`` (see ``main.cpp`` lines 100-111). Quantities of Interest (Outputs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -65,9 +28,9 @@ The simulation extracts four statistical quantities at the final timestep: 1. **max_temp**: Maximum temperature in the domain 2. **mean_temp**: Average temperature across all cells 3. **std_temp**: Standard deviation of temperature -4. **total_energy**: Sum of temperature values (proportional to total thermal energy) +4. **cell_temp**: Temperature in a particular cell, in this case, cell (9,9,9) -These outputs are computed in ``main.cpp`` (lines 293-299) and written to the datalog file. +These outputs are computed in ``main.cpp``and written to the datalog file. PyTUQ Workflow ^^^^^^^^^^^^^^ @@ -82,4 +45,4 @@ PyTUQ uses polynomial chaos expansion to construct a surrogate model: The connection is: - **Inputs**: ParmParse parameters (``diffusion_coeff``, ``init_amplitude``, ``init_variance``) specified in ``inputs`` file or command line -- **Outputs**: Quantities of interest extracted from datalog files or direct Python access to MultiFabs +- **Outputs**: Quantities of interest extracted from datalog files diff --git a/Docs/source/index.rst b/Docs/source/index.rst index 5ed1d232..cba245ae 100644 --- a/Docs/source/index.rst +++ b/Docs/source/index.rst @@ -50,7 +50,7 @@ sorted by the following categories: - :ref:`GPU` -- Offload work to the GPUs using AMReX tools. - :ref:`Linear Solvers` -- Examples of several linear solvers. - :ref:`ML/PYTORCH` -- Use of pytorch models to replace point-wise computational kernels. -- :ref:`AMReX-pytuq` -- Use of pytuq methods to perform generalized sensitivity analysis. +- :ref:`UQ with pytuq` -- Use of pytuq methods to perform generalized sensitivity analysis. - :ref:`MPMD` -- Usage of AMReX-MPMD (Multiple Program Multiple Data) framework. - :ref:`MUI` -- Incorporates the MxUI/MUI (Multiscale Universal interface) frame into AMReX. - :ref:`Particles` -- Basic usage of AMReX's particle data structures. From c7b75342c000a65955dc55c9547bd654f92faee4 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Thu, 22 Jan 2026 16:51:12 -0800 Subject: [PATCH 140/142] documentation --- Docs/source/HeatEquation_UQ.rst | 73 +++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 78498e3d..0382e5e0 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -118,6 +118,79 @@ Understanding GNU Parallel Workflow Pattern Understanding the Output ------------------------ +| `pnames.txt`, `outnames.txt` +| -names of input and output parameters + +| `param_margpc.txt` +| -Each row contains the mean and standard deviation for an uncertain input parameter + +| `qtrain.txt`, `qtest.txt` +| -each row is a separate set of normal random variables used to generate uncertain inputs + +| `ptrain.txt`, `ptest.txt`, `pall.txt` +| -each row is a separate set of input parameters for each simulation + +| `stdoutlog_train##.txt`, `stdoutlog_test##.txt` +| -All standard output from AMReX simulations for training and testing data. The numbers refer to separate simulations. + +| `datalog_train##.txt`, `datalog_test##.txt` +| -Specifically-chosen output from AMReX simulations for training and testing data. In this example it reports the outputs (max, mean, standard deviation, and specific cell temperature) at user-specified intervals. + +| `ytrain.txt`, `ytest.txt`, `yall.txt` +| -agglomeration of outputs of interest from all simulations + +| `results.pk` +| -Python pickle file encapsulating the results + +| `labels.txt` +| -list of labels of simulation types (training or testing) for diagnostic/plot generation purposes + +| `xx__.png` +| -scatter plot of 2 inputs for training and testing + +| `pcoord_1.png`, `pcoord_1_lab_Testing.png`, `pcoord_1_lab_Training.png` +| -graphical representation of how inputs correlate to outputs for each individual simulation + +| `yx_.png`, `yx__log.png` +| -scatter plots of output as a function of each input + +| `yxx_#.png` +| -scatter plots of output a function of multiple inputs + +| `pdf_tri_inputs.png`, `pdf_tri_output.png` +| -PDFs of inputs and outputs + +| `ensemble.png` +| -graphical display of all output values + +| `idm_#_training.png`, `idm_#_testing.png` +| -graphical display of output values + +| `dm_#.png` +| -”data vs model” parity plots for each output; compares the predicted values from a surrogate model or approximation against the true or actual values from the full computational model + +| `fit_s#_training.png`, `fit_s#_testing.png` +| -Shows model vs PC approximation for a single simulation. + +| `pdf_output_#.png`, `pdf_joyplot.png` +| -PDF of output variables + +| `allsens_main.txt`, `sens_main.png` +| -raw data and plot for parameter sensitivities + +| `jsens_#.png`, `Jsens_ave.png` +| -joint sensitivities of output with respect to inputs + +| `sensmat_main.png` +| -sensitivity matrix + +| `pcslices_o#.png` +| -polynomial chaos fits + +| `pccont_o#_d#_d#.png` +| -polynomial chaos fits of output variables with respect to two input variables + + Additional Resources -------------------- From 58f595e745bc85a23c00152023d71a4176e1021b Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Thu, 22 Jan 2026 16:56:23 -0800 Subject: [PATCH 141/142] trailing whitespace --- Docs/source/HeatEquation_UQ.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/source/HeatEquation_UQ.rst b/Docs/source/HeatEquation_UQ.rst index 0382e5e0..b69a3208 100644 --- a/Docs/source/HeatEquation_UQ.rst +++ b/Docs/source/HeatEquation_UQ.rst @@ -40,7 +40,7 @@ The outputs of interest are the maximum temperature, mean temperature, standard :maxdepth: 1 HeatEquation_UQ_MathematicalDetails - + Located in ``amrex-tutorials/ExampleCodes/UQ/HeatEquation``, this example illustrates a complete forward UQ workflow from parameter sampling randomized input parameters to perform sensitivity analysis. By understanding this example, you will have a basis for understanding how to adapt this workflow to your own AMReX application. From bc1ba815b134ff8afa21cca145e95978d5dc1a58 Mon Sep 17 00:00:00 2001 From: Andy Nonaka Date: Thu, 22 Jan 2026 19:43:03 -0800 Subject: [PATCH 142/142] add sphinx-design package --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b3c2a68a..c927928f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: .github/workflows/dependencies/documentation.sh echo "Installing python packages for docs..." python3 -m pip install --upgrade pip - python3 -m pip install sphinx sphinx_rtd_theme breathe sphinxcontrib.bibtex docutils sphinx-copybutton + python3 -m pip install sphinx sphinx_rtd_theme breathe sphinxcontrib.bibtex docutils sphinx-copybutton sphinx-design - name: Install and Build run: |